Вход Регистрация
Файл: app/Classes/Feed.php
Строк: 114
<?php

declare(strict_types=1);

namespace 
AppClasses;

use 
AppModelsComment;
use 
AppModelsFeed as FeedModel;
use 
AppModelsPoll;
use 
IlluminatePaginationPaginator;
use 
IlluminateSupportCollection;
use 
IlluminateSupportHtmlString;
use 
Throwable;

class 
Feed
{
    private 
mixed $user;

    public function 
__construct()
    {
        
$this->user getUser();
    }

    
/**
     * Встроенные типы плюс зарегистрированные модулями
     */
    
private static function allTypes(): array
    {
        return 
array_merge([
            
'comments' => [
                
'class' => Comment::class,
                
'with'  => ['relate''user''files'],
                
'scope' => fn ($query) => $query->visible(),
            ],
        ], 
Registry::$feeds);
    }

    public function 
getFeed(): HtmlString
    
{
        
$allTypes self::allTypes();

        
$enabledTypes array_keys(array_filter(
            
$allTypes,
            static 
fn ($type$key) => setting("feed_{$key}_show"),
            
ARRAY_FILTER_USE_BOTH
        
));

        
$perPage setting('feed_per_page');
        
$currentPage Paginator::resolveCurrentPage();

        
$query FeedModel::query()
            ->
whereIn('relate_type'$enabledTypes)
            ->
orderByDesc('created_at');

        
// Отсекаем невидимые записи каждого типа прямо в запросе,
        // чтобы они не попадали в выборку и не ломали пагинацию
        
foreach ($enabledTypes as $type) {
            
$idQuery $this->visibleIdsQuery($type$allTypes[$type]);

            
$query->where(function ($q) use ($type$idQuery) {
                
$q->where('relate_type''!='$type)
                    ->
orWhereIn('relate_id'$idQuery);
            });
        }

        
$version cache()->get('feed_version'1);
        
$cacheKey "feed_{$version}_{$currentPage}_" implode(','$enabledTypes);

        
$items cache()->remember($cacheKey, (int) setting('feed_cache_time'), function () use ($query$currentPage$perPage$allTypes) {
            
// Берём на одну запись больше, чтобы Paginator определил наличие следующей страницы без отдельного count()
            
$rows $query->skip(($currentPage 1) * $perPage)->take($perPage 1)->get();
            
$grouped $rows->groupBy('relate_type');

            
$loadedModels = [];
            foreach (
$grouped as $type => $typeRows) {
                
$class $allTypes[$type]['class'];
                
$with $allTypes[$type]['with'];
                
$ids $typeRows->pluck('relate_id')->all();

                
$loadedModels[$type] = $class::with($with)->whereIn('id'$ids)->get()->keyBy('id');
            }

            return 
$rows
                
->map(fn ($row) => $loadedModels[$row->relate_type][$row->relate_id] ?? null)
                ->
filter()
                ->
values();
        });

        
$posts = new Paginator($this->render($items), $perPage$currentPage);
        
$posts->setPath(url('/'));

        return new 
HtmlString((string) view('feeds/_feed'compact('posts')));
    }

    
/**
     * Рендер элементов ленты с изоляцией
     */
    
private function render(Collection $items): Collection
    
{
        
$polls $this->loadPolls($items);
        
$allowDownload $this->user || setting('down_guest_download');

        return 
$items
            
->map(function ($post) use ($polls$allowDownload) {
                
$view Registry::$feeds[$post->getMorphClass()]['view'] ?? 'feeds._' $post->getMorphClass();

                try {
                    return 
view($view, [
                        
'post'          => $post,
                        
'polls'         => $polls,
                        
'user'          => $this->user,
                        
'allowDownload' => $allowDownload,
                    ])->
render();
                } catch (
Throwable $e) {
                    
report($e);

                    return 
null;
                }
            })
            ->
filter()
            ->
values();
    }

    
/**
     * Подзапрос видимых id для типа: scope типа либо минимальный рейтинг
     */
    
private function visibleIdsQuery(string $type, array $info)
    {
        
/** @var class-string $class */
        
$class $info['class'];

        
$idQuery $class::query();

        
// Scope задаёт условия видимости (активность, срок) и при необходимости join,
        // чтобы у темы стало доступно поле rating из присоединённого последнего поста
        
if (! empty($info['scope'])) {
            (
$info['scope'])($idQuery);
        }

        if (
$minRating setting("feed_{$type}_rating")) {
            
$idQuery->where('rating''>'$minRating);
        }

        return 
$idQuery->select($idQuery->getModel()->getQualifiedKeyName());
    }

    
/**
     * Load polls
     */
    
private function loadPolls($posts): array
    {
        if (! 
$this->user) {
            return [];
        }

        
$pairs = [];

        foreach (
$posts as $post) {
            
$morphName $post->getMorphClass();

            
// Резолвер указывает, что голосование привязано к связанной записи (тема -> последний пост)
            
if ($resolver Registry::$feeds[$morphName]['poll'] ?? null) {
                if (
$result $resolver($post)) {
                    [
$type$id] = $result;
                    if (
$id) {
                        
$pairs[$type][] = $id;
                    }
                }
                continue;
            }

            
$pairs[$morphName][] = $post->id;
        }

        if (empty(
$pairs)) {
            return [];
        }

        
$query Poll::query()
            ->
where('user_id'$this->user->id)
            ->
where(function ($q) use ($pairs) {
                foreach (
$pairs as $morphName => $ids) {
                    
$q->orWhere(fn ($inner) => $inner->where('relate_type'$morphName)->whereIn('relate_id'$ids));
                }
            });

        
$polls = [];
        foreach (
$query->get(['relate_type''relate_id''vote']) as $poll) {
            
$polls[$poll->relate_type][$poll->relate_id] = $poll->vote;
        }

        return 
$polls;
    }
}
Онлайн: 1
Реклама