PIMS/app/Modules/Movies/Services/UpsertMovieFromProvider.php

173 lines
6.4 KiB
PHP
Raw Normal View History

2025-12-07 03:49:26 +00:00
<?php
namespace App\Modules\Movies\Services;
use App\Modules\Movies\Models\{Movie, Genre, Actor, Director, Studio, Country, Language};
use Illuminate\Support\Facades\DB;
use App\Modules\Movies\Services\Contracts\UpsertMovieServiceInterface;
class UpsertMovieFromProvider implements UpsertMovieServiceInterface
{
/**
* Persist a movie and all relations from normalized provider details.
*
* @param array $details Output of MovieProvider::details
* @param string $mode 'overwrite'|'duplicate'
* @return Movie
*/
public function handle(array $details, string $mode = 'overwrite'): Movie
{
return DB::transaction(function () use ($details, $mode) {
$provider = $details['provider'] ?? null;
$providerId = $details['provider_id'] ?? null;
$movie = null;
if ($mode === 'overwrite' && $provider && $providerId) {
$movie = Movie::query()
->where('provider', $provider)
->where('provider_id', $providerId)
->first();
}
if (!$movie) {
$movie = new Movie();
}
// Fill scalar fields
$movie->fill([
'provider' => $provider,
'provider_id' => $providerId,
'external_ids' => $details['external_ids'] ?? null,
'title' => $details['title'] ?? '',
'original_title' => $details['original_title'] ?? null,
'description' => $details['description'] ?? null,
'poster_url' => $details['poster_url'] ?? null,
'backdrop_url' => $details['backdrop_url'] ?? null,
'rating' => $details['rating'] ?? null,
'release_date' => $details['release_date'] ?? null,
'year' => $details['year'] ?? null,
'runtime' => $details['runtime'] ?? null,
]);
$movie->save();
// Sync relations using names from details payload
$this->syncByNames($movie, Genre::class, 'genres', $details['genres'] ?? []);
// Actors: also persist profile_path when available; backfill if missing
$this->syncActorsWithProfiles(
$movie,
$details['actors'] ?? [],
$details['actors_profiles'] ?? []
);
2025-12-07 03:49:26 +00:00
$this->syncByNames($movie, Director::class, 'directors', $details['directors'] ?? []);
$this->syncByNames($movie, Studio::class, 'studios', $details['studios'] ?? []);
$this->syncByNames($movie, Country::class, 'countries', $details['countries'] ?? []);
$this->syncByNames($movie, Language::class, 'languages', $details['languages'] ?? []);
return $movie->refresh();
});
}
/**
* @param Movie $movie
* @param class-string $modelClass
* @param string $relation
* @param array $names Names to sync
*/
protected function syncByNames(Movie $movie, string $modelClass, string $relation, array $names): void
{
// Create or fetch IDs by name (case-insensitive normalization)
$ids = collect($names)
->filter()
->map(function ($name) use ($modelClass) {
$original = (string) $name;
$normalized = $this->normalizeName($original);
if ($normalized === '') return null;
// Find existing row in a case-insensitive way
/** @var \Illuminate\Database\Eloquent\Model|null $existing */
$existing = $modelClass::query()
->whereRaw('lower(name) = ?', [mb_strtolower($normalized)])
->first();
if ($existing) {
return $existing->getKey();
}
/** @var \Illuminate\Database\Eloquent\Model $model */
$model = new $modelClass();
$model->setAttribute('name', $normalized);
$model->save();
return $model->getKey();
})
->filter()
->values()
->all();
// Sync relation if method exists
if (method_exists($movie, $relation)) {
$movie->{$relation}()->sync($ids);
}
}
/**
* Sync actors and persist their TMDb profile image path when available.
* If an actor already exists without image data, update the record.
*
* @param Movie $movie
* @param array $names List of actor names
* @param array $profiles Map of name => profile_path (relative path from TMDb)
*/
protected function syncActorsWithProfiles(Movie $movie, array $names, array $profiles = []): void
{
$ids = collect($names)
->filter()
->map(function ($name) use ($profiles) {
$original = (string) $name;
$normalized = $this->normalizeName($original);
if ($normalized === '') return null;
/** @var Actor|null $existing */
$existing = Actor::query()
->whereRaw('lower(name) = ?', [mb_strtolower($normalized)])
->first();
$profilePath = $profiles[$original] ?? $profiles[$normalized] ?? null;
if ($existing) {
if ($profilePath && empty($existing->profile_path)) {
$existing->profile_path = $profilePath;
$existing->save();
}
return $existing->getKey();
}
$actor = new Actor();
$actor->setAttribute('name', $normalized);
if ($profilePath) {
$actor->setAttribute('profile_path', $profilePath);
}
$actor->save();
return $actor->getKey();
})
->filter()
->values()
->all();
if (method_exists($movie, 'actors')) {
$movie->actors()->sync($ids);
}
}
2025-12-07 03:49:26 +00:00
/**
* Normalize a person/genre/studio/etc name for de-duplication.
*/
protected function normalizeName(string $name): string
{
// Trim, collapse internal whitespace to single spaces
$name = trim(preg_replace('/\s+/u', ' ', $name) ?? '');
// Leave original casing (providers) but comparisons will be lowercase.
return $name;
}
}