Files

4 lines
2.2 KiB
PHP
Raw Permalink Normal View History

2026-08-29 15:53:53 +03:00
<?php
namespace App\Services;use App\Models\{Project,ProjectCompetitor,SerpCheck};use Illuminate\Support\Collection;
final class CompetitorReportService{public function __construct(private ProjectDomainMatcher$matcher,private VisibilityReportService$visibility){}public function compare(Project$p,Collection$competitors,\DateTimeInterface$from,\DateTimeInterface$to):array{$ids=$p->keywords()->active()->pluck('id');$checks=SerpCheck::query()->whereIn('keyword_id',$ids)->where('status','completed')->whereBetween('checked_at',[$from,$to])->with(['keyword','results'=>fn($q)=>$q->where('result_type','organic')->orderBy('position')])->orderBy('checked_at')->get()->groupBy(fn($c)=>$c->checked_at->toDateString())->flatMap(fn($day)=>$day->groupBy('keyword_id')->map(fn($x)=>$x->last()))->groupBy('keyword_id')->map(fn($x)=>$x->sortByDesc('checked_at')->first())->values();$domains=collect([$p->domain])->merge($competitors->pluck('normalized_domain'));$scores=[];foreach($domains as$d){$ranks=$checks->map(fn($c)=>$this->rank($c,$d));$scores[$d]=round($ranks->sum(fn($r)=>(float)$this->visibility->weight($r))/max(1,$checks->count())*100,2);}$selected=$competitors->first();$rows=$selected?$checks->map(function($c)use($p,$selected){$own=$this->rank($c,$p->domain);$rival=$this->rank($c,$selected->normalized_domain);$leader=match(true){$own===null&&$rival===null=>'neither',$own===null=>'competitor',$rival===null=>'you',$own<$rival=>'you',$rival<$own=>'competitor',default=>'tied'};return['keyword'=>$c->keyword,'own'=>$own,'rival'=>$rival,'leader'=>$leader,'gap'=>$own&&$rival?abs($own-$rival):null,'checked_at'=>$c->checked_at];})->values():collect();return['checks'=>$checks,'scores'=>$scores,'rows'=>$rows,'lead'=>$rows->where('leader','you')->count(),'trail'=>$rows->where('leader','competitor')->count(),'shared'=>$rows->filter(fn($x)=>$x['own']&&$x['rival'])->count(),'unranked'=>$rows->whereNull('own')->count(),'opportunities'=>$rows->filter(fn($x)=>$x['leader']==='competitor'&&$x['rival']!==null&&$x['rival']<=10)->take(10),'strengths'=>$rows->where('leader','you')->take(10)];}private function rank(SerpCheck$c,string$d):?int{$r=$c->results->first(fn($r)=>$r->position>0&&$this->matcher->matches($d,$r->url));return$r?->position;}}