| 1 |
<?php |
| 2 |
|
| 3 |
if (!defined('ABSPATH')) { |
| 4 |
exit; |
| 5 |
} |
| 6 |
|
| 7 |
/** |
| 8 |
* Carries the best rejected match from the matching engines to the captured-404 |
| 9 |
* insert, within one request. |
| 10 |
* |
| 11 |
* The producer and the consumer sit five frames apart -- SpellChecker rejects a |
| 12 |
* candidate deep inside a Levenshtein scan, and NotFoundResponseService writes |
| 13 |
* the captured row after the whole engine run has already returned null -- and |
| 14 |
* every layer in between (MatchingEngine::match(), the orchestrator, the auto |
| 15 |
* redirect handler, the frontend pipeline) has no use for the value. Threading |
| 16 |
* it through them would put a field nobody reads into four signatures and into |
| 17 |
* the public engine interface; a single recorder both sides resolve from the |
| 18 |
* container keeps the contract at the two ends that care, and lets any future |
| 19 |
* engine record a near miss without an interface change. |
| 20 |
* |
| 21 |
* Two admission rules make a wrong score impossible rather than unlikely: |
| 22 |
* |
| 23 |
* 1. Records are keyed by the requested URL and read back by the same key. |
| 24 |
* A lookup for a different URL returns null, so a leftover record can |
| 25 |
* never be attributed to the wrong captured row -- the failure mode is |
| 26 |
* "no score", which is exactly the behaviour that predates this class. |
| 27 |
* 2. Only a score inside the (0, 100] band is kept. The spelling score is |
| 28 |
* 100 - (distance / basis * 100), which goes negative for a candidate far |
| 29 |
* longer than the request; a negative or absurd confidence is not a |
| 30 |
* confidence, and NULL says so honestly. |
| 31 |
* |
| 32 |
* Only the single best (highest) near miss per URL is kept: that is the closest |
| 33 |
* the plugin came to redirecting, which is the number the admin needs. |
| 34 |
*/ |
| 35 |
class ABJ_404_Solution_NearMissRecorder { |
| 36 |
|
| 37 |
/** Scores must be above this to be recorded (exclusive). */ |
| 38 |
const MIN_RECORDABLE_SCORE = 0.0; |
| 39 |
|
| 40 |
/** Scores must be at or below this to be recorded (inclusive). */ |
| 41 |
const MAX_RECORDABLE_SCORE = 100.0; |
| 42 |
|
| 43 |
/** @var string|null The URL the current record belongs to. */ |
| 44 |
private $requestedURL = null; |
| 45 |
|
| 46 |
/** @var ABJ_404_Solution_NearMissMatch|null Best near miss for that URL. */ |
| 47 |
private $best = null; |
| 48 |
|
| 49 |
/** |
| 50 |
* Record a match an engine found and then rejected for scoring under its |
| 51 |
* threshold. Out-of-band scores are dropped, so every engine can call this |
| 52 |
* unconditionally on its reject branch without vetting the number first. |
| 53 |
* |
| 54 |
* @param array{requestedURL: string, score: float, engineName: string} $match |
| 55 |
* requestedURL is the URL being resolved, as the frontend |
| 56 |
* pipeline spells it (MatchRequest::getRequestedURL(), which is the |
| 57 |
* same string NotFoundResponseService::sendTo404Page() receives). |
| 58 |
* @return void |
| 59 |
*/ |
| 60 |
public function record(array $match): void { |
| 61 |
$requestedURL = $match['requestedURL']; |
| 62 |
$score = $match['score']; |
| 63 |
$engineName = $match['engineName']; |
| 64 |
if (!self::isRecordableScore($score)) { |
| 65 |
return; |
| 66 |
} |
| 67 |
|
| 68 |
if ($this->requestedURL !== $requestedURL) { |
| 69 |
$this->requestedURL = $requestedURL; |
| 70 |
$this->best = null; |
| 71 |
} |
| 72 |
|
| 73 |
if ($this->best === null || $score > $this->best->getScore()) { |
| 74 |
$this->best = ABJ_404_Solution_NearMissMatch::create($score, $engineName); |
| 75 |
} |
| 76 |
} |
| 77 |
|
| 78 |
/** |
| 79 |
* The best near miss recorded for this exact URL, or null when none was |
| 80 |
* recorded for it (automatic matching off, no candidate at all, or a |
| 81 |
* record that belongs to a different URL). |
| 82 |
* |
| 83 |
* @param string $requestedURL |
| 84 |
* @return ABJ_404_Solution_NearMissMatch|null |
| 85 |
*/ |
| 86 |
public function getBestFor(string $requestedURL): ?ABJ_404_Solution_NearMissMatch { |
| 87 |
if ($this->requestedURL !== $requestedURL) { |
| 88 |
return null; |
| 89 |
} |
| 90 |
return $this->best; |
| 91 |
} |
| 92 |
|
| 93 |
/** |
| 94 |
* @param float $score |
| 95 |
* @return bool |
| 96 |
*/ |
| 97 |
public static function isRecordableScore(float $score): bool { |
| 98 |
return $score > self::MIN_RECORDABLE_SCORE && $score <= self::MAX_RECORDABLE_SCORE; |
| 99 |
} |
| 100 |
} |
| 101 |
|