198 lines
6.1 KiB
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])),
|
|
};
|
|
}
|
|
}
|