, array> */ public const RELATIONS = [ Page::class => [], Project::class => ['technologies', 'services', 'members'], Service::class => ['technologies', 'projects'], Call::class => ['technologies'], ]; /** * Never copied: where it stands rather than what it says. */ protected const RESET = [ 'published_at', 'unpublish_at', 'review_requested_at', 'review_requested_by', 'reviewer_id', ]; public static function supports(Model $record): bool { return array_key_exists($record::class, self::RELATIONS); } public static function duplicate(Model $record): Model { if (! self::supports($record)) { throw new InvalidArgumentException($record::class.' cannot be duplicated.'); } // Read again: a table row carries aggregates — view counts, summed // stars — that are not columns and cannot be inserted. $record = $record->fresh() ?? $record; return DB::transaction(function () use ($record): Model { $copy = $record->replicate(array_values(array_filter( self::RESET, fn (string $column) => array_key_exists($column, $record->getAttributes()), ))); $titles = []; $slugs = []; foreach ((array) $record->getTranslations('title') as $locale => $title) { if (filled($title)) { $titles[$locale] = $title.' '.__('panel.duplicate.title_suffix', [], $locale); } } foreach ((array) $record->getTranslations('slug') as $locale => $slug) { if (filled($slug)) { $slugs[$locale] = self::freeSlug($record, $locale, $slug.'-'.__('panel.duplicate.slug_suffix', [], $locale)); } } $copy->setTranslations('title', $titles); $copy->setTranslations('slug', $slugs); match (true) { $copy instanceof Page => $copy->forceFill(['status' => PageStatus::Draft, 'author_id' => Auth::id() ?? $record->author_id]), $copy instanceof Service => $copy->forceFill(['is_published' => false]), default => null, }; $copy->save(); foreach (self::RELATIONS[$record::class] as $relation) { $related = $record->{$relation}()->get(); $copy->{$relation}()->sync( $relation === 'members' ? $related->mapWithKeys(fn ($member) => [$member->id => ['role' => $member->pivot->role]])->all() : $related->pluck('id')->all(), ); } return $copy; }); } /** * The slug, or the slug with a number after it, whichever nobody has. */ protected static function freeSlug(Model $record, string $locale, string $slug): string { $candidate = $slug; $n = 2; $query = fn (string $value) => $record::query() ->when(method_exists($record, 'bootSoftDeletes'), fn ($q) => $q->withTrashed()) ->where("slug->{$locale}", $value) ->exists(); while ($query($candidate)) { $candidate = "{$slug}-{$n}"; $n++; } return $candidate; } }