Proje dosyaları eklendi
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
namespace App\Http\Controllers;
|
||||
use App\Enums\{KeywordStatus,ProjectStatus,SerpCheckStatus};use App\Http\Requests\{StoreKeywordRequest,UpdateKeywordRequest};use App\Models\{Keyword,Project};use App\Services\{KeywordAuditLogger,KeywordPhraseNormalizer,NextCheckAtCalculator};use App\Support\KeywordTargetOptions;use Illuminate\Database\QueryException;use Illuminate\Http\{RedirectResponse,Request};use Illuminate\Support\Facades\DB;use Illuminate\Validation\{Rule,ValidationException};use Illuminate\View\View;
|
||||
class KeywordController extends Controller{
|
||||
public function __construct(private readonly KeywordAuditLogger $keywordAuditLogger,private readonly NextCheckAtCalculator $nextCheckAtCalculator){}
|
||||
public function legacyShow(Project $customerProject,Keyword $customerKeyword):View{return $this->show(request(),$customerKeyword);}
|
||||
public function legacyEdit(Project $customerProject,Keyword $customerKeyword):View{return $this->edit(request(),$customerKeyword);}
|
||||
public function legacyUpdate(UpdateKeywordRequest $r,Project $customerProject,Keyword $customerKeyword,KeywordPhraseNormalizer $n):RedirectResponse{return $this->update($r,$n);}
|
||||
public function legacyArchive(Request $r,Project $customerProject,Keyword $customerKeyword):RedirectResponse{return $this->archive($r,$customerKeyword);}
|
||||
public function legacyRestore(Request $r,Project $customerProject,Keyword $customerKeyword):RedirectResponse{return $this->restore($r,$customerKeyword);}
|
||||
public function index(Request $r):View{
|
||||
$this->authorize('viewAny',Keyword::class);$projects=$r->user()->projects()->orderBy('name')->get();$nested=$r->route('customerProject');$projectUlid=$nested?->ulid?:$r->string('project')->toString();
|
||||
$q=Keyword::query()->whereHas('project',fn($x)=>$x->where('user_id',$r->user()->id))->with(['project','serpChecks'=>fn($x)=>$x->where('status',SerpCheckStatus::Completed)->latest('checked_at')->with(['results'=>fn($y)=>$y->where('is_project_domain',true)->orderBy('position')])]);
|
||||
if($projectUlid)$q->whereHas('project',fn($x)=>$x->where('ulid',$projectUlid));if($s=trim($r->string('q')->toString()))$q->where('phrase','like','%'.addcslashes($s,'%_').'%');
|
||||
foreach(['country_code'=>'country','language_code'=>'language','device'=>'device','search_engine'=>'engine'] as $column=>$key)if($v=$r->string($key)->toString())$q->where($column,$v);
|
||||
$requestedStatus=$r->input('status',$r->input('filter'));$status=in_array($requestedStatus,['active','inactive','archived'],true)?$requestedStatus:null;if($status==='archived')$q->where('status',KeywordStatus::Archived)->whereNotNull('archived_at');elseif($status)$q->where('status',$status)->whereNull('archived_at');else$q->whereIn('status',[KeywordStatus::Active,KeywordStatus::Inactive])->whereNull('archived_at');$sort=in_array($r->input('sort'),['phrase','created_at','updated_at'],true)?$r->input('sort'):'created_at';$direction=$r->input('direction')==='asc'?'asc':'desc';$keywords=$q->orderBy($sort,$direction)->paginate(15)->withQueryString();
|
||||
$keywords->getCollection()->each(fn($k)=>$this->rankData($k));$all=(clone $q)->get();$all->each(fn($k)=>$this->rankData($k));$ranked=$all->whereNotNull('current_rank');$summary=['total'=>$all->count(),'active'=>$all->where('status',KeywordStatus::Active)->count(),'top10'=>$ranked->where('current_rank','<=',10)->count(),'average'=>$ranked->isEmpty()?null:round($ranked->avg('current_rank'),1),'winners'=>$all->where('movement','winner')->count(),'losers'=>$all->where('movement','loser')->count()];
|
||||
return view('keywords.index',compact('keywords','projects','summary','projectUlid')+$this->options());
|
||||
}
|
||||
public function create(Request $r):View{$project=$r->route('customerProject')?:$r->user()->projects()->active()->where('ulid',$r->input('project'))->first();$projects=$r->user()->projects()->active()->orderBy('name')->get();abort_if($projects->isEmpty(),404);$project??=$projects->first();$this->authorize('create',[Keyword::class,$project]);return view('keywords.create',compact('project','projects')+$this->options());}
|
||||
public function store(StoreKeywordRequest $r,KeywordPhraseNormalizer $normalizer):RedirectResponse{
|
||||
$project=$r->project();$seen=[];$targets=[];$duplicates=0;$rejected=0;foreach(preg_split('/\R/u',$r->string('phrases')->toString())?:[] as $line){if(trim($line)==='')continue;try{$data=$normalizer->normalize($line);}catch(\InvalidArgumentException){$rejected++;continue;}$key=$data['normalized_phrase'];if(isset($seen[$key])){$duplicates++;continue;}$seen[$key]=true;$target=$data+['search_engine'=>$r->input('search_engine'),'country_code'=>strtoupper($r->input('country_code')),'language_code'=>strtolower($r->input('language_code')),'device'=>$r->input('device')];if($project->keywords()->where($target)->exists()){$duplicates++;continue;}$targets[]=$target;}
|
||||
$errorKey=$r->has('phrase')?'phrase':'phrases';if(!$targets)throw ValidationException::withMessages([$errorKey=>__('keywords.validation.none_created')]);$created=DB::transaction(function()use($targets,$project,$r,$errorKey){$created=[];foreach($targets as$target){try{$created[]=$project->keywords()->create($target);}catch(QueryException){continue;}}if(!$created)throw ValidationException::withMessages([$errorKey=>__('keywords.validation.none_created')]);foreach($created as$k)$this->keywordAuditLogger->record(count($created)>1?'bulk_created':'created',$k,$r->user());return$created;});return redirect()->route('keywords.index',['project'=>$project->ulid])->with('status',__('keywords.created_result',['created'=>count($created),'duplicates'=>$duplicates,'rejected'=>$rejected]));
|
||||
}
|
||||
public function show(Request $r,?Keyword $keyword=null):View{$keyword=$this->keyword($r,$keyword);$this->authorize('view',$keyword);$keyword->load(['project','serpChecks'=>fn($q)=>$q->where('status',SerpCheckStatus::Completed)->latest('checked_at')->with(['results'=>fn($x)=>$x->orderBy('position')])]);$this->rankData($keyword);$history=$keyword->serpChecks->map(fn($c)=>['date'=>$c->checked_at?->toIso8601String(),'rank'=>$c->results->firstWhere('is_project_domain',true)?->position])->filter(fn($x)=>$x['rank']!==null)->reverse()->values();$best=$history->min('rank');return view('keywords.show',compact('keyword','history','best')+$this->options());}
|
||||
public function edit(Request $r,?Keyword $keyword=null):View{$keyword=$this->keyword($r,$keyword);$this->authorize('update',$keyword);abort_if($keyword->status===KeywordStatus::Archived||$keyword->archived_at!==null,422);return view('keywords.edit',['keyword'=>$keyword,'project'=>$keyword->project]+$this->options());}
|
||||
public function update(UpdateKeywordRequest $r,KeywordPhraseNormalizer $n):RedirectResponse{$k=$r->keyword();$before=$k->attributesToArray();$status=KeywordStatus::from($r->input('status'));$data=$n->normalize($r->string('phrase'))+['country_code'=>strtoupper($r->input('country_code')),'language_code'=>strtolower($r->input('language_code')),'device'=>$r->input('device')];$changed=collect($data)->contains(function($v,$key)use($k){$current=$k->{$key};return($current instanceof \BackedEnum?$current->value:(string)$current)!==$v;});try{DB::transaction(function()use($k,$data,$r,$before,$changed,$status){if($changed&&$k->serpChecks()->exists()){$k->archive();$this->keywordAuditLogger->record('archived_for_retarget',$k,$r->user());$new=$k->project->keywords()->create($data+['search_engine'=>$k->search_engine->value]);$new->forceFill(['status'=>$status,'tracking_enabled'=>$k->tracking_enabled,'check_frequency'=>$k->check_frequency,'next_check_at'=>$status===KeywordStatus::Active&&$k->tracking_enabled?$this->nextCheckAtCalculator->calculate($k->check_frequency):null])->save();$this->keywordAuditLogger->record('created_from_retarget',$new,$r->user());}else{$k->forceFill($data+['status'=>$status,'next_check_at'=>$status===KeywordStatus::Active&&$k->tracking_enabled?$this->nextCheckAtCalculator->calculate($k->check_frequency):null])->save();$this->keywordAuditLogger->record('updated',$k,$r->user(),$before);}});}catch(QueryException){throw ValidationException::withMessages(['phrase'=>__('keywords.validation.duplicate')]);}return redirect()->route('keywords.index',['project'=>$k->project->ulid])->with('status',__('keywords.updated'));}
|
||||
public function status(Request $r,Keyword $keyword):RedirectResponse{$this->authorize('update',$keyword);abort_if($keyword->status===KeywordStatus::Archived||$keyword->archived_at!==null,422);$data=$r->validate(['status'=>['required',Rule::in([KeywordStatus::Active->value,KeywordStatus::Inactive->value])]]);$status=KeywordStatus::from($data['status']);if($keyword->status===$status)return back()->with('status',__('keywords.updated'));DB::transaction(function()use($keyword,$status,$r){$before=$keyword->attributesToArray();$keyword->forceFill(['status'=>$status,'next_check_at'=>$status===KeywordStatus::Active&&$keyword->tracking_enabled?$this->nextCheckAtCalculator->calculate($keyword->check_frequency):null])->save();$this->keywordAuditLogger->record('status_changed',$keyword,$r->user(),$before);});return back()->with('status',__('keywords.updated'));}
|
||||
public function archive(Request $r,?Keyword $keyword=null):RedirectResponse{$k=$this->keyword($r,$keyword);$this->authorize('update',$k);if($k->status===KeywordStatus::Archived&&$k->archived_at!==null)return back()->with('status',__('keywords.archived'));DB::transaction(function()use($k,$r){$before=$k->attributesToArray();$k->archive();$this->keywordAuditLogger->record('archived',$k,$r->user(),$before);});return back()->with('status',__('keywords.archived'));}
|
||||
public function restore(Request $r,?Keyword $keyword=null):RedirectResponse{$k=$this->keyword($r,$keyword);$this->authorize('update',$k);if($k->status===KeywordStatus::Active&&$k->archived_at===null)return back()->with('status',__('keywords.restored'));abort_unless($k->project->status===ProjectStatus::Active&&$k->project->archived_at===null,422);$duplicate=$k->project->keywords()->active()->where('normalized_phrase',$k->normalized_phrase)->where('search_engine',$k->search_engine)->where('country_code',$k->country_code)->where('language_code',$k->language_code)->where('device',$k->device)->whereKeyNot($k->id)->exists();if($duplicate)throw ValidationException::withMessages(['keyword'=>__('keywords.validation.restore_duplicate')]);DB::transaction(function()use($k,$r){$before=$k->attributesToArray();$k->restoreKeyword();$this->keywordAuditLogger->record('restored',$k,$r->user(),$before);});return back()->with('status',__('keywords.restored'));}
|
||||
private function keyword(Request $r,?Keyword $keyword):Keyword{return $keyword?:($r->route('customerKeyword')?:$r->route('keyword'));}
|
||||
private function rankData(Keyword $k):void{$checks=$k->serpChecks->values();$current=$checks->get(0)?->results->firstWhere('is_project_domain',true)?->position;$previous=$checks->get(1)?->results->firstWhere('is_project_domain',true)?->position;$k->setAttribute('current_rank',$current);$k->setAttribute('previous_rank',$previous);$k->setAttribute('rank_change',$current&&$previous?$previous-$current:null);$k->setAttribute('movement',$current===null||$previous===null?'new':($current<$previous?'winner':($current>$previous?'loser':'unchanged')));$k->setAttribute('last_checked_at',$checks->get(0)?->checked_at);}
|
||||
private function options():array{return ['countries'=>KeywordTargetOptions::countries(),'languages'=>KeywordTargetOptions::languages()];}
|
||||
}
|
||||
Reference in New Issue
Block a user