MIF_E31231430/app/Models/Lensa.php

198 lines
6.1 KiB
PHP

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
class Lensa extends Model
{
public const DEFAULT_STOCK = 2;
private const IMAGE_DIRECTORY = 'images/kameras';
private static ?array $cachedImagePaths = null;
protected $fillable = [
'kode_lensa', 'nama', 'merek', 'harga_sewa', 'stok',
'aperture_score', 'stabilisasi', 'fleksibilitas'
];
public function getImagePathAttribute(): ?string
{
$images = self::availableImages();
if ($images === []) {
return null;
}
$brand = is_string($this->merek) ? $this->merek : '';
$name = is_string($this->nama) ? $this->nama : '';
$sourceName = Str::startsWith(Str::lower($name), Str::lower($brand)) ? $name : trim($brand.' '.$name);
$fullCompactVariants = self::compactVariants($sourceName);
$modelCompactVariants = self::compactVariants($name);
$brandVariants = self::brandVariants($brand);
$bestMatch = null;
$bestScore = 0;
foreach ($images as $image) {
$score = self::scoreImageMatch($image, $fullCompactVariants, $modelCompactVariants, $brandVariants);
if ($score > $bestScore) {
$bestScore = $score;
$bestMatch = $image['path'];
}
}
return $bestScore >= 70 ? $bestMatch : null;
}
private static function availableImages(): array
{
if (self::$cachedImagePaths !== null) {
return self::$cachedImagePaths;
}
$directory = public_path(self::IMAGE_DIRECTORY);
if (! File::isDirectory($directory)) {
return self::$cachedImagePaths = [];
}
return self::$cachedImagePaths = collect(File::files($directory))
->filter(fn ($file) => in_array(strtolower($file->getExtension()), ['jpg', 'jpeg', 'png', 'webp'], true))
->map(function ($file): array {
return [
'path' => self::IMAGE_DIRECTORY.'/'.$file->getFilename(),
'compact' => self::compactString(pathinfo($file->getFilename(), PATHINFO_FILENAME)),
'priority' => self::extensionPriority($file->getExtension()),
'length' => strlen($file->getFilename()),
];
})
->sortBy([
['priority', 'asc'],
['length', 'asc'],
['path', 'asc'],
])
->values()
->all();
}
private static function scoreImageMatch(array $image, array $fullCompactVariants, array $modelCompactVariants, array $brandVariants): int
{
$score = 0;
$fileCompact = $image['compact'];
if ($brandVariants !== [] && ! collect($brandVariants)->contains(fn ($brand) => Str::contains($fileCompact, $brand))) {
return 0;
}
foreach ($fullCompactVariants as $variant) {
if ($variant !== '' && $fileCompact === $variant) {
return 160;
}
if ($variant !== '' && Str::contains($fileCompact, $variant)) {
$score = max($score, 125);
}
}
foreach ($modelCompactVariants as $variant) {
if ($variant !== '' && $fileCompact === $variant) {
return 150;
}
if ($variant !== '' && Str::contains($fileCompact, $variant)) {
$score = max($score, 115);
}
}
$meaningfulTokens = self::meaningfulTokens($modelCompactVariants[0] ?? '');
$matchedTokens = array_values(array_filter(
$meaningfulTokens,
fn ($token) => Str::contains($fileCompact, $token)
));
if ($meaningfulTokens !== [] && count($matchedTokens) === count($meaningfulTokens)) {
$score = max($score, 95);
} elseif (count($matchedTokens) >= 3) {
$score = max($score, 75);
}
return $score;
}
private static function compactVariants(string $value): array
{
$normalized = Str::lower(Str::ascii($value));
$baseVariants = [
$normalized,
preg_replace('/\bf\s*\/?\s*\d+(?:\.\d+)?\b/u', '', $normalized) ?? $normalized,
];
$variants = [];
$queue = array_map(self::compactString(...), $baseVariants);
while ($queue !== []) {
$variant = array_shift($queue);
if ($variant === '' || in_array($variant, $variants, true)) {
continue;
}
$variants[] = $variant;
foreach (['canon', 'sony', 'fujifilm', 'fujinon', 'nikon', 'nikkor', 'lensa'] as $brand) {
if (Str::startsWith($variant, $brand)) {
$queue[] = Str::after($variant, $brand);
}
}
if (Str::contains($variant, 'mount')) {
$queue[] = str_replace('mount', '', $variant);
}
}
return $variants;
}
private static function compactString(string $value): string
{
return (string) preg_replace('/[^a-z0-9]+/', '', Str::lower(Str::ascii($value)));
}
private static function meaningfulTokens(string $compact): array
{
preg_match_all('/[a-z]+|\d+/', $compact, $matches);
return array_values(array_filter(
$matches[0] ?? [],
fn ($token) => strlen($token) >= 2 || ctype_digit($token)
));
}
private static function extensionPriority(string $extension): int
{
return match (strtolower($extension)) {
'png' => 1,
'jpg', 'jpeg' => 2,
'webp' => 3,
default => 9,
};
}
private static function brandVariants(string $brand): array
{
$brandCompact = self::compactString($brand);
return match ($brandCompact) {
'canon' => ['canon'],
'sony' => ['sony'],
'fujifilm' => ['fujifilm', 'fujinon'],
'nikon' => ['nikon', 'nikkor'],
default => array_values(array_filter([$brandCompact])),
};
}
}