slug = $pr->generateUniqueSlug($pr->title, [ * 'portal' => $pr->portal, * 'language' => $pr->language, * ]); * * @mixin Model */ trait HasUniqueSlug { /** * Build a unique slug from `$source`. Optionally pass overrides for the * scoping attributes (matched against {@see slugScopeAttributes()}). * * @param array $scope */ public function generateUniqueSlug(string $source, array $scope = []): string { $base = Str::slug($source) ?: $this->slugFallback(); $slug = $base; $suffix = 2; while ($this->slugExists($slug, $scope)) { $slug = $base.'-'.$suffix++; } return $slug; } /** * @param array $scope */ protected function slugExists(string $slug, array $scope): bool { /** @var Model $self */ $self = $this; $query = $self::query() ->withoutGlobalScopes() ->where($this->slugColumn(), $slug); if ($self->exists) { $query->where($self->getKeyName(), '!=', $self->getKey()); } foreach ($this->slugScopeAttributes() as $attribute) { $value = array_key_exists($attribute, $scope) ? $scope[$attribute] : $self->getAttribute($attribute); $value !== null ? $query->where($attribute, $value) : $query->whereNull($attribute); } $this->applySlugConstraints($query); return $query->exists(); } /** * @return list */ protected function slugScopeAttributes(): array { return []; } protected function slugColumn(): string { return 'slug'; } protected function slugFallback(): string { return 'item'; } /** * Hook for models to add additional WHERE constraints (e.g. soft-deleted * records). Default: no additional constraints. */ protected function applySlugConstraints(Builder $query): void { // no-op } }