117 lines
2.9 KiB
PHP
117 lines
2.9 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\{BelongsTo, HasMany, BelongsToMany, HasOne};
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
|
|
class Category extends Model
|
|
{
|
|
use SoftDeletes;
|
|
|
|
protected $fillable = [
|
|
'content_type_id','parent_id','name','slug',
|
|
'description','image','is_active','sort_order'
|
|
];
|
|
|
|
protected $casts = ['is_active' => 'boolean'];
|
|
|
|
/**
|
|
* Ensure slug is always lowercase and valid before saving.
|
|
*/
|
|
protected static function boot()
|
|
{
|
|
parent::boot();
|
|
|
|
static::saving(function (Category $model) {
|
|
if (isset($model->slug)) {
|
|
$model->slug = strtolower($model->slug);
|
|
if (!preg_match('/^[a-z0-9-]+$/', $model->slug)) {
|
|
throw new \InvalidArgumentException('Category slug must be lowercase and contain only a-z, 0-9, and dashes.');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
public function contentType(): BelongsTo
|
|
{
|
|
return $this->belongsTo(ContentType::class);
|
|
}
|
|
|
|
public function parent(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Category::class, 'parent_id');
|
|
}
|
|
|
|
public function children(): HasMany
|
|
{
|
|
return $this->hasMany(Category::class, 'parent_id')
|
|
->orderBy('sort_order')->orderBy('name');
|
|
}
|
|
|
|
public function descendants(): HasMany
|
|
{
|
|
return $this->children()->with('descendants');
|
|
}
|
|
|
|
public function seo(): HasOne
|
|
{
|
|
return $this->hasOne(CategorySeo::class);
|
|
}
|
|
|
|
public function artworks(): BelongsToMany
|
|
{
|
|
return $this->belongsToMany(Artwork::class, 'artwork_category');
|
|
}
|
|
|
|
public function scopeActive($query)
|
|
{
|
|
return $query->where('is_active', true);
|
|
}
|
|
|
|
public function scopeRoots($query)
|
|
{
|
|
return $query->whereNull('parent_id');
|
|
}
|
|
|
|
public function getFullSlugPathAttribute(): string
|
|
{
|
|
return $this->parent
|
|
? $this->parent->full_slug_path . '/' . $this->slug
|
|
: $this->slug;
|
|
}
|
|
|
|
/**
|
|
* Get the full public URL for this category (authoritative spec).
|
|
* Example: /photography/abstract/dark
|
|
*/
|
|
public function getUrlAttribute(): string
|
|
{
|
|
$contentTypeSlug = strtolower($this->contentType->slug);
|
|
$path = strtolower($this->full_slug_path);
|
|
return '/' . $contentTypeSlug . ($path ? '/' . $path : '');
|
|
}
|
|
|
|
/**
|
|
* Get the canonical URL for SEO (authoritative spec).
|
|
* Example: https://skinbase.org/photography/abstract/dark
|
|
*/
|
|
public function getCanonicalUrlAttribute(): string
|
|
{
|
|
return 'https://skinbase.org' . $this->url;
|
|
}
|
|
|
|
public function getBreadcrumbsAttribute(): array
|
|
{
|
|
return $this->parent
|
|
? array_merge($this->parent->breadcrumbs, [$this])
|
|
: [$this];
|
|
}
|
|
|
|
public function getRouteKeyName(): string
|
|
{
|
|
return 'slug';
|
|
}
|
|
}
|