b2in/app/Models/CmsBrand.php

348 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Spatie\Translatable\HasTranslations;
/**
* Kernmarke mit eigener Detailseite (CABINET, Rotpunkt Küchen, Febrü …).
*
* Nicht zu verwechseln mit {@see Brand} – das ist die Produkt-/Partner-Marke.
*/
class CmsBrand extends Model
{
use HasFactory, HasTranslations;
/** Tatsaechlich ausgefuehrtes Projekt – ein Foto aus einem echten Auftrag. */
public const GALLERY_PROJECT = 'project';
/** Entwurf, Rendering oder KI-gestuetzte Darstellung – nicht realisiert. */
public const GALLERY_INSPIRATION = 'inspiration';
/** Einblick in Werkstatt, Fertigung oder Montage des Herstellers – weder Projekt noch Entwurf (Nachtrag 19). */
public const GALLERY_COMPANY = 'company';
/** @var list<string> */
public const GALLERY_CATEGORIES = [self::GALLERY_PROJECT, self::GALLERY_INSPIRATION, self::GALLERY_COMPANY];
protected $table = 'cms_brands';
protected $fillable = [
'slug',
'name',
'tagline',
'teaser',
'summary',
'highlights',
'description',
'logo',
'image',
'services',
'sections',
'widget_label',
'cta_title',
'cta_text',
'gallery',
'website_url',
'showroom',
'origin_city',
'origin_distance',
'origin_role',
'is_published',
'order',
];
/** @var array<string> */
public array $translatable = [
'name',
'tagline',
'teaser',
'summary',
'description',
'widget_label',
'cta_title',
'cta_text',
'origin_city',
'origin_distance',
'origin_role',
];
protected function casts(): array
{
return [
'highlights' => 'array',
'services' => 'array',
'sections' => 'array',
'gallery' => 'array',
'is_published' => 'boolean',
'order' => 'integer',
];
}
public function scopePublished(Builder $query): Builder
{
return $query->where('is_published', true);
}
public function scopeOrdered(Builder $query): Builder
{
return $query->orderBy('order')->orderBy('id');
}
/**
* Veröffentlichte Kernmarken fürs Frontend, nach Slug indiziert.
*
* Das CMS gewinnt. Solange keine Marke gepflegt ist, greift die Lang-Sektion `core_brands`
* als Fallback – dieselbe Reihenfolge wie bei cms_theme_section(), damit die Startseite auf
* einer frischen Installation nicht ohne Kernmarken dasteht.
*
* @return array<string, array<string, mixed>>
*/
public static function forFrontend(): array
{
$brands = static::query()->published()->ordered()->get();
if ($brands->isNotEmpty()) {
return $brands
->mapWithKeys(fn (self $brand) => [$brand->slug => $brand->toFrontendArray()])
->all();
}
return static::fallbackFromLang();
}
/**
* @return array<string, array<string, mixed>>
*/
private static function fallbackFromLang(): array
{
$section = cms_theme_section('core_brands');
$brands = is_array($section) ? ($section['brands'] ?? []) : [];
$out = [];
foreach ($brands as $brand) {
if (! is_array($brand) || empty($brand['name'])) {
continue;
}
$slug = $brand['slug'] ?? \Illuminate\Support\Str::slug((string) $brand['name']);
$entry = array_merge([
'slug' => $slug,
'tagline' => '',
'teaser' => '',
'summary' => '',
'highlights' => [],
'description' => '',
'logo' => '',
'image' => '',
'services' => [],
'sections' => [],
'widget_label' => '',
'cta_title' => '',
'cta_text' => '',
'gallery' => [],
'website_url' => '',
'showroom' => '',
'origin_city' => '',
'origin_distance' => '',
'origin_role' => '',
'has_detail_page' => false,
], $brand, ['slug' => $slug, 'has_detail_page' => false]);
$entry['gallery'] = self::normalizeGallery($entry['gallery']);
$entry['sections'] = self::normalizeSections($entry['sections']);
$out[$slug] = $entry;
}
return $out;
}
/**
* Seitenabschnitte in eine feste Form bringen.
*
* Jeder Abschnitt hat einen Titel und Absätze. Optional dazu:
* - `claim` hervorgehobene Schlusszeile
* - `quote` Zitat mit `quote_author`
* - `detail` Vertiefung, die im Frontend aufklappbar dargestellt wird
*
* @param mixed $sections
* @return list<array{title: string, paragraphs: list<string>, claim: string, quote: string, quote_author: string, detail: array{title: string, paragraphs: list<string>}|null}>
*/
public static function normalizeSections($sections): array
{
if (! is_array($sections)) {
return [];
}
$out = [];
foreach ($sections as $section) {
if (! is_array($section)) {
continue;
}
$paragraphs = self::paragraphList($section['paragraphs'] ?? []);
$title = trim((string) ($section['title'] ?? ''));
if ($title === '' && $paragraphs === []) {
continue;
}
$detail = null;
if (is_array($section['detail'] ?? null)) {
$detailParagraphs = self::paragraphList($section['detail']['paragraphs'] ?? []);
$detailTitle = trim((string) ($section['detail']['title'] ?? ''));
if ($detailTitle !== '' || $detailParagraphs !== []) {
$detail = ['title' => $detailTitle, 'paragraphs' => $detailParagraphs];
}
}
$out[] = [
'title' => $title,
'paragraphs' => $paragraphs,
'claim' => trim((string) ($section['claim'] ?? '')),
'quote' => trim((string) ($section['quote'] ?? '')),
'quote_author' => trim((string) ($section['quote_author'] ?? '')),
'detail' => $detail,
];
}
return $out;
}
/**
* @param mixed $paragraphs
* @return list<string>
*/
private static function paragraphList($paragraphs): array
{
if (is_string($paragraphs)) {
$paragraphs = preg_split('/\R{2,}/', $paragraphs) ?: [];
}
if (! is_array($paragraphs)) {
return [];
}
return array_values(array_filter(array_map(
fn ($p) => trim((string) (is_array($p) ? ($p['text'] ?? '') : $p)),
$paragraphs,
)));
}
/**
* Galerie-Eintraege in eine feste Form bringen.
*
* Ein Eintrag ohne (oder mit unbekannter) Kategorie gilt als Inspiration, nicht als Projekt:
* Ein Entwurf, der versehentlich als Referenz durchgeht, waere die schaedlichere Verwechslung.
*
* @param mixed $gallery
* @return list<array{image: string, category: string, caption: string}>
*/
public static function normalizeGallery($gallery): array
{
if (! is_array($gallery)) {
return [];
}
$out = [];
foreach ($gallery as $item) {
$image = is_array($item) ? (string) ($item['image'] ?? '') : (string) $item;
$image = trim($image);
if ($image === '') {
continue;
}
$category = is_array($item) ? (string) ($item['category'] ?? '') : '';
$out[] = [
'image' => $image,
'category' => in_array($category, self::GALLERY_CATEGORIES, true)
? $category
: self::GALLERY_INSPIRATION,
'caption' => is_array($item) ? trim((string) ($item['caption'] ?? '')) : '',
];
}
return $out;
}
/**
* Galerie nach realisierten Projekten, Einblicken in die Werkstatt und Inspirationen getrennt.
*
* @param mixed $gallery
* @return array{projects: list<array{image: string, category: string, caption: string}>, company: list<array{image: string, category: string, caption: string}>, inspirations: list<array{image: string, category: string, caption: string}>}
*/
public static function groupGallery($gallery): array
{
$items = self::normalizeGallery($gallery);
return [
'projects' => array_values(array_filter(
$items,
fn (array $item) => $item['category'] === self::GALLERY_PROJECT,
)),
'company' => array_values(array_filter(
$items,
fn (array $item) => $item['category'] === self::GALLERY_COMPANY,
)),
'inspirations' => array_values(array_filter(
$items,
fn (array $item) => $item['category'] === self::GALLERY_INSPIRATION,
)),
];
}
/**
* Beschriftung des Badges auf dem Galeriebild („Realisiertes Projekt“, „Aus der Werkstatt“, „Visualisierung“).
*/
public static function galleryBadge(string $category): string
{
return match ($category) {
self::GALLERY_PROJECT => __('ui.brand_gallery_badge_project'),
self::GALLERY_COMPANY => __('ui.brand_gallery_badge_company'),
default => __('ui.brand_gallery_badge_inspiration'),
};
}
/**
* @return array<string, mixed>
*/
public function toFrontendArray(): array
{
return [
'id' => $this->id,
'slug' => $this->slug,
'name' => $this->name,
'tagline' => $this->tagline,
'teaser' => $this->teaser,
'summary' => $this->summary,
'highlights' => $this->highlights ?? [],
'description' => $this->description,
'logo' => $this->logo,
'image' => $this->image,
'services' => $this->services ?? [],
'sections' => self::normalizeSections($this->sections),
'widget_label' => (string) $this->widget_label,
'cta_title' => (string) $this->cta_title,
'cta_text' => (string) $this->cta_text,
'gallery' => self::normalizeGallery($this->gallery),
'website_url' => $this->website_url,
'showroom' => $this->showroom,
'origin_city' => (string) $this->origin_city,
'origin_distance' => (string) $this->origin_distance,
'origin_role' => (string) $this->origin_role,
'has_detail_page' => true,
];
}
}