Files
mtdb_movie/app/Actions/Titles/Store/StoreCredits.php
maher 703f50a09d
Some checks failed
Build / run (push) Has been cancelled
first commit
2025-10-29 11:42:25 +01:00

210 lines
7.6 KiB
PHP
Executable File

<?php
namespace App\Actions\Titles\Store;
use App\Models\Episode;
use App\Models\Person;
use App\Models\Season;
use App\Models\Title;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
class StoreCredits
{
public const UNIQUE_KEY = 'tmdb_id';
private Title|Episode|Season|Person|null $model = null;
public function __construct(private Person $person, private Title $title)
{
}
/**
* @param array $originalMediaItems
*/
public function execute(Title|Episode|Season|Person $model, $originalMediaItems)
{
$this->model = $model;
if (empty($originalMediaItems)) return;
// generate records we will insert into "creditables" pivot table
$newPivotRecords = $this->generatePivotRecords($originalMediaItems);
if ($newPivotRecords->isEmpty()) return;
// fetch all existing "creditable" pivot rows for this creditable
$existingPivotRecords = $this->getExistingRecords($newPivotRecords);
// merge new and existing pivot records, so "order" column value is not lost
$mergedPivotRecords = $this->mergeNewAndExistingRecords($existingPivotRecords, $newPivotRecords);
// delete all "creditables" pivot table records for this creditable
$this->detachExistingRecords($existingPivotRecords);
// insert new pivot records
DB::table('creditables')->insert($mergedPivotRecords->toArray());
}
/**
* @param array $originalMediaItems
* @return Collection
*/
private function generatePivotRecords($originalMediaItems)
{
// need to remove series, otherwise it will attach all guest starts to main series, because
// tmdb API does not specify whether credit belongs to series, season or episode.
$originalMediaItems = $this->filterOutSeries($originalMediaItems);
$dbMediaItems = $this->insertOrRetrieveMediaItems($originalMediaItems);
return collect($originalMediaItems)->map(function($originalMediaItem) use($dbMediaItems) {
$creditable = $dbMediaItems->first(fn($item, $uniqueKey) => $uniqueKey === $originalMediaItem[self::UNIQUE_KEY] && $item['is_series'] === Arr::get($originalMediaItem, 'is_series'));
if ( ! $creditable) return null;
// either attaching multiple titles to a person
// or attaching multiple people to title/season/episode
if ($this->model->model_type === Person::MODEL_TYPE) {
$personId = $this->model->id;
$creditableId = $creditable->id;
} else {
$personId = $creditable->id;
$creditableId = $this->model->id;
}
// build relation records for attaching all credits to title
// (same person might be attached to title multiple times)
return [
'id' => null,
'person_id' => $personId,
'creditable_id' => $creditableId,
'creditable_type' => $this->getCreditableType(),
'character' => Arr::get($originalMediaItem, 'relation_data.character'),
'order' => Arr::get($originalMediaItem, 'relation_data.order'),
'department' => Arr::get($originalMediaItem, 'relation_data.department'),
'job' => Arr::get($originalMediaItem, 'relation_data.job'),
];
})->filter()->values();
}
/**
* Merge existing "creditables" pivot table records with the ones
* we are about to insert. This needs to be done because sometimes
* "order" property does not exist on "new" records, but exists on
* "old" ones and since we will delete "old" ones, "order" would be lost.
*
* @param Collection $existingRecords
* @param Collection $newRecords
* @return Collection
*/
private function mergeNewAndExistingRecords($existingRecords, $newRecords)
{
// all of these properties on both arrays must match for them to be considered equal
$matchProps = collect(['person_id', 'creditable_id', 'creditable_type', 'character', 'department', 'job']);
return $newRecords->map(function($newRecord) use($existingRecords, $matchProps) {
$oldRecord = $existingRecords->first(fn($existingRecord) => $matchProps->every(fn($prop) => $existingRecord[$prop] === $newRecord[$prop]), []);
return $this->arrayMergeIfNotNull($newRecord, $oldRecord);
});
}
/**
* @param array $arr1
* @param array $arr2
* @return array
*/
private function arrayMergeIfNotNull($arr1, $arr2) {
foreach($arr2 as $key => $val) {
$is_set_and_not_null = isset($arr1[$key]);
if ( $val == null && $is_set_and_not_null ) {
$arr2[$key] = $arr1[$key];
}
}
return array_merge($arr1, $arr2);
}
/**
* Insert or retrieve titles or people that need to be attached.
*
* @param Collection|(Title|Person)[] $mediaItems
* @return Collection
*/
private function insertOrRetrieveMediaItems(Collection|array $mediaItems)
{
// make sure we only insert person/title once, even
// if they appear multiple time in title credits
$uniqueMediaItems = collect($mediaItems)->unique(self::UNIQUE_KEY)->values();
if ($uniqueMediaItems->isEmpty()) return collect();
if ($uniqueMediaItems[0]['model_type'] === Person::MODEL_TYPE) {
$mediaItems = $this->person->insertOrRetrieve($uniqueMediaItems);
} else {
$mediaItems = $this->title->insertOrRetrieve($uniqueMediaItems);
}
return $mediaItems->mapWithKeys(fn($item) => [$item[self::UNIQUE_KEY] => $item]);
}
/**
* @param Collection $records
* @return Collection
*/
private function getExistingRecords($records)
{
// select only fields needed to do the diff
return DB::table('creditables')
->whereIn('person_id', $records->pluck('person_id'))
->whereIn('job', $records->pluck('job'))
->where('creditable_type', $records[0]['creditable_type'])
->whereIn('creditable_id', $records->pluck('creditable_id'))
->get()
->map(fn($record) => (array) $record);
}
/**
* Delete creditable records that already exist.
*
* @param Collection $records
*/
private function detachExistingRecords($records)
{
if ($records->isEmpty()) return;
// select only fields needed to do the diff
$existingRecords = DB::table('creditables')
->whereIn('person_id', $records->pluck('person_id'))
->whereIn('job', $records->pluck('job'))
->where('creditable_type', $records[0]['creditable_type'])
->whereIn('creditable_id', $records->pluck('creditable_id'))
->get();
DB::table('creditables')->whereIn('id', $existingRecords->pluck('id'))->delete();
}
/**
* Get creditable type for morphToMany or morphedByMany relation.
*/
private function getCreditableType(): string
{
if ($this->model::class === Person::class) {
// won't be attaching seasons or episodes here
// so can just return title type instantly
return Title::MODEL_TYPE;
} else {
return $this->model::MODEL_TYPE;
}
}
/**
* Remove all series and episodes from specified array.
*
* @param array $originalMediaItems
* @return array
*/
private function filterOutSeries($originalMediaItems)
{
return array_filter($originalMediaItems, fn($creditable) => !Arr::get($creditable, 'is_series'));
}
}