Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,14 @@ parameters:
- src/

# Level 9 is the highest level
level: 6
level: 8

# This is a package: traits are intended to be used by consumers, so PHPStan
# can't always see their usages within this repository.
ignoreErrors:
- identifier: trait.unused
- identifier: generics.lessTypes
path: src/Translatable/Contracts/Translatable.php
- identifier: missingType.return
path: src/Translatable/Contracts/Translatable.php
paths:
- src/Translatable/Translatable.php
- src/Translatable/Traits/Relationship.php
- src/Translatable/Traits/Scopes.php

# Avoid "always true/false" when the type comes from PHPDoc.
treatPhpDocTypesAsCertain: false
6 changes: 3 additions & 3 deletions src/Translatable/Contracts/Translatable.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public function isTranslationAttribute(string $key): bool;
*/
public function replicateWithTranslations(?array $except = null): Model;

public function setDefaultLocale(?string $locale);
public function setDefaultLocale(?string $locale): static;

public function translate(?string $locale = null, bool $withFallback = false): ?Model;

Expand All @@ -54,12 +54,12 @@ public function translateOrDefault(?string $locale = null): ?Model;
public function translateOrNew(?string $locale = null): Model;

/**
* @return HasOne<Model>
* @return HasOne<Model, Model>
*/
public function translation(): HasOne;

/**
* @return HasMany<Model>
* @return HasMany<Model, Model>
*/
public function translations(): HasMany;
}
3 changes: 3 additions & 0 deletions src/Translatable/Locales.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ public function getLanguageFromCountryBasedLocale(string $locale): string
return explode($this->getLocaleSeparator(), $locale)[0];
}

/**
* @return non-empty-string
*/
public function getLocaleSeparator(): string
{
return $this->config->get('translatable.locale_separator') ?: '-';
Expand Down
11 changes: 10 additions & 1 deletion src/Translatable/Traits/Relationship.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Astrotomic\Translatable\Traits;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;

Expand All @@ -22,6 +23,8 @@ public function getRelationKey(): string

/**
* @internal will change to protected
*
* @return class-string<Model>
*/
public function getTranslationModelName(): string
{
Expand Down Expand Up @@ -62,6 +65,9 @@ public function getTranslationRelationKey(): string
return $this->getForeignKey();
}

/**
* @return HasOne<Model, $this>
*/
public function translation(): HasOne
{
return $this
Expand All @@ -73,12 +79,15 @@ public function translation(): HasOne
});
}

/**
* @return HasMany<Model, $this>
*/
public function translations(): HasMany
{
return $this->hasMany($this->getTranslationModelName(), $this->getTranslationRelationKey());
}

protected function localeOrFallback()
protected function localeOrFallback(): ?string
{
return $this->useFallback() && ! $this->translations()->where($this->getLocaleKey(), $this->locale())->exists()
? $this->getFallbackLocale()
Expand Down
59 changes: 49 additions & 10 deletions src/Translatable/Traits/Scopes.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@

trait Scopes
{
public function scopeListsTranslations(Builder $query, string $translationField)
/**
* @param Builder<static> $query
* @return Builder<static>
*/
public function scopeListsTranslations(Builder $query, string $translationField): Builder
{
$withFallback = $this->useFallback();
$translationTable = $this->getTranslationsTable();
Expand Down Expand Up @@ -39,7 +43,11 @@ public function scopeListsTranslations(Builder $query, string $translationField)
return $query;
}

public function scopeNotTranslatedIn(Builder $query, ?string $locale = null)
/**
* @param Builder<static> $query
* @return Builder<static>
*/
public function scopeNotTranslatedIn(Builder $query, ?string $locale = null): Builder
{
$locale = $locale ?: $this->locale();

Expand All @@ -48,7 +56,11 @@ public function scopeNotTranslatedIn(Builder $query, ?string $locale = null)
});
}

public function scopeOrderByTranslation(Builder $query, string $translationField, string $sortMethod = 'asc')
/**
* @param Builder<static> $query
* @return Builder<static>
*/
public function scopeOrderByTranslation(Builder $query, string $translationField, string $sortMethod = 'asc'): Builder
{
$translationTable = $this->getTranslationsTable();
$localeKey = $this->getLocaleKey();
Expand All @@ -66,22 +78,38 @@ public function scopeOrderByTranslation(Builder $query, string $translationField
->orderBy("{$translationTable}.{$translationField}", $sortMethod);
}

public function scopeOrWhereTranslation(Builder $query, string $translationField, $value, ?string $locale = null)
/**
* @param Builder<static> $query
* @return Builder<static>
*/
public function scopeOrWhereTranslation(Builder $query, string $translationField, mixed $value, ?string $locale = null): Builder
{
return $this->scopeWhereTranslation($query, $translationField, $value, $locale, 'orWhereHas');
}

public function scopeOrWhereTranslationLike(Builder $query, string $translationField, $value, ?string $locale = null)
/**
* @param Builder<static> $query
* @return Builder<static>
*/
public function scopeOrWhereTranslationLike(Builder $query, string $translationField, mixed $value, ?string $locale = null): Builder
{
return $this->scopeWhereTranslation($query, $translationField, $value, $locale, 'orWhereHas', 'LIKE');
}

public function scopeTranslated(Builder $query)
/**
* @param Builder<static> $query
* @return Builder<static>
*/
public function scopeTranslated(Builder $query): Builder
{
return $query->has('translations');
}

public function scopeTranslatedIn(Builder $query, ?string $locale = null)
/**
* @param Builder<static> $query
* @return Builder<static>
*/
public function scopeTranslatedIn(Builder $query, ?string $locale = null): Builder
{
$locale = $locale ?: $this->locale();

Expand All @@ -90,7 +118,11 @@ public function scopeTranslatedIn(Builder $query, ?string $locale = null)
});
}

public function scopeWhereTranslation(Builder $query, string $translationField, $value, ?string $locale = null, string $method = 'whereHas', string $operator = '=')
/**
* @param Builder<static> $query
* @return Builder<static>
*/
public function scopeWhereTranslation(Builder $query, string $translationField, mixed $value, ?string $locale = null, string $method = 'whereHas', string $operator = '='): Builder
{
return $query->$method('translations', function (Builder $query) use ($translationField, $value, $locale, $operator) {
$query->where($this->getTranslationsTable().'.'.$translationField, $operator, $value);
Expand All @@ -101,12 +133,19 @@ public function scopeWhereTranslation(Builder $query, string $translationField,
});
}

public function scopeWhereTranslationLike(Builder $query, string $translationField, $value, ?string $locale = null)
/**
* @param Builder<static> $query
* @return Builder<static>
*/
public function scopeWhereTranslationLike(Builder $query, string $translationField, mixed $value, ?string $locale = null): Builder
{
return $this->scopeWhereTranslation($query, $translationField, $value, $locale, 'whereHas', 'LIKE');
}

public function scopeWithTranslation(Builder $query, ?string $locale = null)
/**
* @param Builder<static> $query
*/
public function scopeWithTranslation(Builder $query, ?string $locale = null): void
{
$locale = $locale ?: $this->locale();

Expand Down
35 changes: 21 additions & 14 deletions src/Translatable/Translatable.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,21 @@ trait Translatable
{
use Relationship, Scopes;

protected static $autoloadTranslations = null;
protected static ?bool $autoloadTranslations = null;

protected static $deleteTranslationsCascade = false;
protected static bool $deleteTranslationsCascade = false;

protected $defaultLocale;
protected ?string $defaultLocale = null;

public static function bootTranslatable(): void
{
static::saved(function (Model $model) {
/* @var Translatable $model */
return $model->saveTranslations();
static::saved(function (self $model): void {
$model->saveTranslations();
});

static::deleting(function (Model $model) {
/* @var Translatable $model */
static::deleting(function (self $model): void {
if (self::$deleteTranslationsCascade === true) {
return $model->deleteTranslations();
$model->deleteTranslations();
}
});
}
Expand Down Expand Up @@ -95,9 +93,9 @@ public function attributesToArray()
}

/**
* @param string|array|null $locales The locales to be deleted
* @param null|string|array<string> $locales The locales to be deleted
*/
public function deleteTranslations($locales = null): void
public function deleteTranslations(string|array|null $locales = null): void
{
if ($locales === null) {
$translations = $this->translations()->get();
Expand Down Expand Up @@ -262,6 +260,9 @@ public function getTranslationOrFail(string $locale): Model
return $translation;
}

/**
* @return array<string,array<string,mixed>>
*/
public function getTranslationsArray(): array
{
$translations = [];
Expand Down Expand Up @@ -298,6 +299,9 @@ public function isWrapperAttribute(string $key): bool
return $key === config('translatable.translations_wrapper');
}

/**
* @param null|array<string> $except
*/
public function replicateWithTranslations(?array $except = null): Model
{
$newInstance = $this->replicate($except);
Expand All @@ -324,7 +328,7 @@ public function setAttribute($key, $value)
return parent::setAttribute($key, $value);
}

public function setDefaultLocale(?string $locale)
public function setDefaultLocale(?string $locale): static
{
$this->defaultLocale = $locale;

Expand Down Expand Up @@ -356,7 +360,7 @@ protected function getLocalesHelper(): Locales
return app(Locales::class);
}

protected function isEmptyTranslatableAttribute(string $key, $value): bool
protected function isEmptyTranslatableAttribute(string $key, mixed $value): bool
{
return empty($value);
}
Expand Down Expand Up @@ -401,6 +405,9 @@ protected function saveTranslations(): bool
return $saved;
}

/**
* @return array{0:string,1:string}
*/
protected function getAttributeAndLocale(string $key): array
{
if (Str::contains($key, ':')) {
Expand All @@ -410,7 +417,7 @@ protected function getAttributeAndLocale(string $key): array
return [$key, $this->locale()];
}

protected function getAttributeOrFallback(?string $locale, string $attribute)
protected function getAttributeOrFallback(?string $locale, string $attribute): mixed
{
$translation = $this->getTranslation($locale);

Expand Down
3 changes: 3 additions & 0 deletions src/Translatable/TranslatableServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Astrotomic\Translatable\Validation\Rules\TranslatableExists;
use Astrotomic\Translatable\Validation\Rules\TranslatableUnique;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Rule;

Expand All @@ -29,9 +30,11 @@ public function register(): void
);

Rule::macro('translatableUnique', function (string $model, string $field): TranslatableUnique {
/** @var class-string<Model> $model */
return new TranslatableUnique($model, $field);
});
Rule::macro('translatableExists', function (string $model, string $field): TranslatableExists {
/** @var class-string<Model> $model */
return new TranslatableExists($model, $field);
});

Expand Down
10 changes: 8 additions & 2 deletions src/Translatable/Validation/RuleFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ public function parse(array $input): array
continue;
}

foreach ($this->locales as $locale) {
foreach (($this->locales ?? []) as $locale) {
$rules[$this->formatKey($locale, $key)] = $this->formatRule($locale, $value);
}
}
Expand Down Expand Up @@ -143,7 +143,13 @@ protected function formatRule(string $locale, $rule)

protected function replacePlaceholder(string $locale, string $value): string
{
return preg_replace($this->getPattern(), $this->getReplacement($locale), $value);
$result = preg_replace($this->getPattern(), $this->getReplacement($locale), $value);

if ($result === null) {
throw new InvalidArgumentException("The rule [{$value}] is not valid.");
}

return $result;
}

protected function getReplacement(string $locale): string
Expand Down
Loading