optimizations

This commit is contained in:
2026-03-28 19:15:39 +01:00
parent 0b25d9570a
commit cab4fbd83e
509 changed files with 1016804 additions and 1605 deletions

View File

@@ -0,0 +1,423 @@
<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\Collection;
use App\Models\CollectionSurfaceDefinition;
use App\Models\CollectionSurfacePlacement;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection as SupportCollection;
class CollectionSurfaceService
{
public function definitions(): SupportCollection
{
return CollectionSurfaceDefinition::query()->orderBy('surface_key')->get();
}
public function placements(?string $surfaceKey = null): SupportCollection
{
$query = CollectionSurfacePlacement::query()
->with([
'collection.user:id,username,name',
'collection.coverArtwork:id,user_id,title,slug,hash,thumb_ext,published_at,is_public,is_approved,deleted_at',
])
->orderBy('surface_key')
->orderByDesc('priority')
->orderBy('id');
if ($surfaceKey !== null && $surfaceKey !== '') {
$query->where('surface_key', $surfaceKey);
}
return $query->get();
}
public function placementConflicts(?string $surfaceKey = null): SupportCollection
{
return $this->placements($surfaceKey)
->where('is_active', true)
->groupBy('surface_key')
->flatMap(function (SupportCollection $placements, string $key): array {
$conflicts = [];
$values = $placements->values();
$count = $values->count();
for ($leftIndex = 0; $leftIndex < $count; $leftIndex++) {
$left = $values[$leftIndex];
for ($rightIndex = $leftIndex + 1; $rightIndex < $count; $rightIndex++) {
$right = $values[$rightIndex];
if (! $this->placementsOverlap($left, $right)) {
continue;
}
$conflicts[] = [
'surface_key' => $key,
'placement_ids' => [(int) $left->id, (int) $right->id],
'collection_ids' => [(int) $left->collection_id, (int) $right->collection_id],
'collection_titles' => [
$left->collection?->title ?? 'Unknown collection',
$right->collection?->title ?? 'Unknown collection',
],
'summary' => sprintf(
'%s overlaps with %s on %s.',
$left->collection?->title ?? 'Unknown collection',
$right->collection?->title ?? 'Unknown collection',
$key,
),
'window' => [
'starts_at' => $this->earliestStart($left, $right)?->toISOString(),
'ends_at' => $this->latestEnd($left, $right)?->toISOString(),
],
];
}
}
return $conflicts;
})
->values();
}
public function upsertDefinition(array $attributes): CollectionSurfaceDefinition
{
return CollectionSurfaceDefinition::query()->updateOrCreate(
['surface_key' => (string) $attributes['surface_key']],
[
'title' => (string) $attributes['title'],
'description' => $attributes['description'] ?? null,
'mode' => (string) ($attributes['mode'] ?? 'manual'),
'rules_json' => $attributes['rules_json'] ?? null,
'ranking_mode' => (string) ($attributes['ranking_mode'] ?? 'ranking_score'),
'max_items' => (int) ($attributes['max_items'] ?? 12),
'is_active' => (bool) ($attributes['is_active'] ?? true),
'starts_at' => $attributes['starts_at'] ?? null,
'ends_at' => $attributes['ends_at'] ?? null,
'fallback_surface_key' => $attributes['fallback_surface_key'] ?? null,
]
);
}
public function populateSurface(string $surfaceKey, int $fallbackLimit = 12): SupportCollection
{
return $this->resolveSurfaceItems($surfaceKey, $fallbackLimit);
}
public function resolveSurfaceItems(string $surfaceKey, int $fallbackLimit = 12): SupportCollection
{
return $this->resolveSurfaceItemsInternal($surfaceKey, $fallbackLimit, []);
}
public function syncPlacements(): int
{
return CollectionSurfacePlacement::query()
->where('is_active', true)
->where(function ($query): void {
$query->whereNotNull('ends_at')->where('ends_at', '<=', now());
})
->update([
'is_active' => false,
'updated_at' => now(),
]);
}
public function upsertPlacement(array $attributes): CollectionSurfacePlacement
{
$placementId = isset($attributes['id']) ? (int) $attributes['id'] : null;
$payload = [
'surface_key' => (string) $attributes['surface_key'],
'collection_id' => (int) $attributes['collection_id'],
'placement_type' => (string) ($attributes['placement_type'] ?? 'manual'),
'priority' => (int) ($attributes['priority'] ?? 0),
'starts_at' => $attributes['starts_at'] ?? null,
'ends_at' => $attributes['ends_at'] ?? null,
'is_active' => (bool) ($attributes['is_active'] ?? true),
'campaign_key' => $attributes['campaign_key'] ?? null,
'notes' => $attributes['notes'] ?? null,
'created_by_user_id' => isset($attributes['created_by_user_id']) ? (int) $attributes['created_by_user_id'] : null,
];
if ($placementId) {
$placement = CollectionSurfacePlacement::query()->findOrFail($placementId);
$placement->fill($payload)->save();
return $placement->refresh();
}
return CollectionSurfacePlacement::query()->create($payload);
}
public function deletePlacement(CollectionSurfacePlacement $placement): void
{
$placement->delete();
}
private function resolveSurfaceItemsInternal(string $surfaceKey, int $fallbackLimit, array $visited): SupportCollection
{
if (in_array($surfaceKey, $visited, true)) {
return collect();
}
$visited[] = $surfaceKey;
$definition = CollectionSurfaceDefinition::query()->where('surface_key', $surfaceKey)->first();
$limit = max(1, min((int) ($definition?->max_items ?? $fallbackLimit), 24));
$mode = (string) ($definition?->mode ?? 'manual');
if ($definition && ! $this->definitionIsActive($definition)) {
return $this->resolveFallbackSurface($definition, $fallbackLimit, $visited);
}
$manual = CollectionSurfacePlacement::query()
->with([
'collection.user:id,username,name',
'collection.coverArtwork:id,user_id,title,slug,hash,thumb_ext,published_at,is_public,is_approved,deleted_at',
])
->where('surface_key', $surfaceKey)
->where('is_active', true)
->where(function ($query): void {
$query->whereNull('starts_at')->orWhere('starts_at', '<=', now());
})
->where(function ($query): void {
$query->whereNull('ends_at')->orWhere('ends_at', '>', now());
})
->orderByDesc('priority')
->orderBy('id')
->limit($limit)
->get()
->pluck('collection')
->filter(fn (?Collection $collection) => $collection && $collection->isFeatureablePublicly())
->values();
if ($mode === 'manual') {
return $manual->isEmpty()
? $this->resolveFallbackSurface($definition, $fallbackLimit, $visited)
: $manual;
}
$query = Collection::query()->publicEligible();
$rules = is_array($definition?->rules_json) ? $definition->rules_json : [];
$this->applyAutomaticRules($query, $rules);
$rankingMode = (string) ($definition?->ranking_mode ?? 'ranking_score');
if ($rankingMode === 'recent_activity') {
$query->orderByDesc('last_activity_at');
} elseif ($rankingMode === 'quality_score') {
$query->orderByDesc('quality_score');
} else {
$query->orderByDesc('ranking_score');
}
$auto = $query
->when($mode === 'hybrid', fn ($builder) => $builder->whereNotIn('id', $manual->pluck('id')->all()))
->with([
'user:id,username,name',
'coverArtwork:id,user_id,title,slug,hash,thumb_ext,published_at,is_public,is_approved,deleted_at',
])
->limit($mode === 'hybrid' ? max(0, $limit - $manual->count()) : $limit)
->get();
if ($mode === 'automatic') {
return $auto->isEmpty()
? $this->resolveFallbackSurface($definition, $fallbackLimit, $visited)
: $auto->values();
}
if ($manual->count() >= $limit) {
return $manual;
}
$resolved = $manual->concat($auto)->values();
return $resolved->isEmpty()
? $this->resolveFallbackSurface($definition, $fallbackLimit, $visited)
: $resolved;
}
private function applyAutomaticRules(Builder $query, array $rules): void
{
$this->applyExactOrListRule($query, 'type', $rules['type'] ?? null);
$this->applyExactOrListRule($query, 'campaign_key', $rules['campaign_key'] ?? null);
$this->applyExactOrListRule($query, 'event_key', $rules['event_key'] ?? null);
$this->applyExactOrListRule($query, 'season_key', $rules['season_key'] ?? null);
$this->applyExactOrListRule($query, 'presentation_style', $rules['presentation_style'] ?? null);
$this->applyExactOrListRule($query, 'theme_token', $rules['theme_token'] ?? null);
$this->applyExactOrListRule($query, 'collaboration_mode', $rules['collaboration_mode'] ?? null);
$this->applyExactOrListRule($query, 'promotion_tier', $rules['promotion_tier'] ?? null);
if ($this->ruleEnabled($rules['featured_only'] ?? false)) {
$query->where('is_featured', true);
}
if ($this->ruleEnabled($rules['commercial_eligible_only'] ?? false)) {
$query->where('commercial_eligibility', true);
}
if ($this->ruleEnabled($rules['analytics_enabled_only'] ?? false)) {
$query->where('analytics_enabled', true);
}
if (($minQualityScore = $this->numericRule($rules['min_quality_score'] ?? null)) !== null) {
$query->where('quality_score', '>=', $minQualityScore);
}
if (($minRankingScore = $this->numericRule($rules['min_ranking_score'] ?? null)) !== null) {
$query->where('ranking_score', '>=', $minRankingScore);
}
$includeIds = $this->integerRuleList($rules['include_collection_ids'] ?? null);
if ($includeIds !== []) {
$query->whereIn('id', $includeIds);
}
$excludeIds = $this->integerRuleList($rules['exclude_collection_ids'] ?? null);
if ($excludeIds !== []) {
$query->whereNotIn('id', $excludeIds);
}
$ownerUsernames = $this->stringRuleList($rules['owner_usernames'] ?? ($rules['owner_username'] ?? null));
if ($ownerUsernames !== []) {
$normalized = array_map(static fn (string $value): string => mb_strtolower($value), $ownerUsernames);
$query->whereHas('user', function (Builder $builder) use ($normalized): void {
$builder->whereIn('username', $normalized);
});
}
}
private function applyExactOrListRule(Builder $query, string $column, mixed $value): void
{
$values = $this->stringRuleList($value);
if ($values === []) {
return;
}
if (count($values) === 1) {
$query->where($column, $values[0]);
return;
}
$query->whereIn($column, $values);
}
private function stringRuleList(mixed $value): array
{
$values = is_array($value) ? $value : [$value];
return array_values(array_unique(array_filter(array_map(static function ($item): ?string {
if (! is_string($item) && ! is_numeric($item)) {
return null;
}
$normalized = trim((string) $item);
return $normalized !== '' ? $normalized : null;
}, $values))));
}
private function integerRuleList(mixed $value): array
{
$values = is_array($value) ? $value : [$value];
return array_values(array_unique(array_filter(array_map(static function ($item): ?int {
if (! is_numeric($item)) {
return null;
}
$normalized = (int) $item;
return $normalized > 0 ? $normalized : null;
}, $values))));
}
private function numericRule(mixed $value): ?float
{
if (! is_numeric($value)) {
return null;
}
return (float) $value;
}
private function ruleEnabled(mixed $value): bool
{
if (is_bool($value)) {
return $value;
}
if (is_numeric($value)) {
return (int) $value === 1;
}
if (is_string($value)) {
return in_array(mb_strtolower(trim($value)), ['1', 'true', 'yes', 'on'], true);
}
return false;
}
private function definitionIsActive(CollectionSurfaceDefinition $definition): bool
{
if (! $definition->is_active) {
return false;
}
if ($definition->starts_at && $definition->starts_at->isFuture()) {
return false;
}
if ($definition->ends_at && $definition->ends_at->lessThanOrEqualTo(now())) {
return false;
}
return true;
}
private function resolveFallbackSurface(?CollectionSurfaceDefinition $definition, int $fallbackLimit, array $visited): SupportCollection
{
$fallbackKey = $definition?->fallback_surface_key;
if (! is_string($fallbackKey) || trim($fallbackKey) === '') {
return collect();
}
return $this->resolveSurfaceItemsInternal(trim($fallbackKey), $fallbackLimit, $visited);
}
private function placementsOverlap(CollectionSurfacePlacement $left, CollectionSurfacePlacement $right): bool
{
$leftStart = $left->starts_at?->getTimestamp() ?? PHP_INT_MIN;
$leftEnd = $left->ends_at?->getTimestamp() ?? PHP_INT_MAX;
$rightStart = $right->starts_at?->getTimestamp() ?? PHP_INT_MIN;
$rightEnd = $right->ends_at?->getTimestamp() ?? PHP_INT_MAX;
return $leftStart < $rightEnd && $rightStart < $leftEnd;
}
private function earliestStart(CollectionSurfacePlacement $left, CollectionSurfacePlacement $right)
{
if ($left->starts_at === null) {
return $right->starts_at;
}
if ($right->starts_at === null) {
return $left->starts_at;
}
return $left->starts_at->lessThanOrEqualTo($right->starts_at) ? $left->starts_at : $right->starts_at;
}
private function latestEnd(CollectionSurfacePlacement $left, CollectionSurfacePlacement $right)
{
if ($left->ends_at === null || $right->ends_at === null) {
return null;
}
return $left->ends_at->greaterThanOrEqualTo($right->ends_at) ? $left->ends_at : $right->ends_at;
}
}