PluginProbe
404 Solution / trunk
404 Solution vtrunk
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / spelling / SpellCandidateFilter.php

SpellCandidateFilter.php in 404 Solution trunk, at includes/spelling/SpellCandidateFilter.php

233 lines 8.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7 /**
8 * Candidate filtering, scoring, and matching on posts/tags/categories
9 * for the spell-checking subsystem.
10 *
11 * Extracted from SpellCheckerTrait_CandidateFiltering as a standalone class
12 * with explicit dependency injection.
13 */
14 class ABJ_404_Solution_SpellCandidateFilter {
15
16 /** @var ABJ_404_Solution_Functions */
17 private $f;
18
19 /** @var ABJ_404_Solution_ContentRepository */
20 private $contentRepository;
21
22 /** @var ABJ_404_Solution_SpellURLMatcher */
23 private $urlMatcher;
24
25 /** @var ABJ_404_Solution_SpellLevenshteinEngine */
26 private $levenshteinEngine;
27
28 /** @var ABJ_404_Solution_SpellPostListeners */
29 private $postListeners;
30
31 /** @var ABJ_404_Solution_SpellSuggestionExclusionPolicy */
32 private $exclusionPolicy;
33
34 /** @var ABJ_404_Solution_SpellSuggestionScorer */
35 private $suggestionScorer;
36
37 /** @var array<int, string> */
38 private array $separatingCharacters;
39
40 /**
41 * @param ABJ_404_Solution_Functions $functions
42 * @param ABJ_404_Solution_PluginLogic $logic
43 * @param ABJ_404_Solution_Logging $logger
44 * @param ABJ_404_Solution_ContentRepository $contentRepository
45 * @param ABJ_404_Solution_SpellURLMatcher $urlMatcher
46 * @param ABJ_404_Solution_SpellLevenshteinEngine $levenshteinEngine
47 * @param ABJ_404_Solution_SpellPostListeners $postListeners
48 * @param string|int|null $custom404PageID
49 * @param array<int, string> $separatingCharacters
50 * @param array<int, string> $separatingCharactersForImages
51 */
52 public function __construct(
53 $functions, $logic, $logger, $contentRepository,
54 $urlMatcher, $levenshteinEngine, $postListeners,
55 $custom404PageID, array $separatingCharacters, array $separatingCharactersForImages
56 ) {
57 $this->f = $functions;
58 $this->contentRepository = $contentRepository;
59 $this->urlMatcher = $urlMatcher;
60 $this->levenshteinEngine = $levenshteinEngine;
61 $this->postListeners = $postListeners;
62 $this->separatingCharacters = $separatingCharacters;
63 $this->exclusionPolicy = new ABJ_404_Solution_SpellSuggestionExclusionPolicy(
64 $functions, $logic, $logger, $urlMatcher, $custom404PageID
65 );
66 // Build the term candidate source around THIS scorer's own
67 // $contentRepository (its full-scan fallback target), pulling only the
68 // n-gram collaborators from the container. Resolving a fully
69 // container-wired source instead would bind the fallback to the
70 // container's content_repository, which can differ from the repository
71 // this scorer was constructed with (e.g. test doubles).
72 $ngramFilterResolved = self::resolveOptionalService('ngram_filter');
73 $publishedTermsProviderResolved = self::resolveOptionalService('published_terms_provider');
74 $termCoveragePolicyResolved = self::resolveOptionalService('term_ngram_coverage_policy');
75 $termCandidateSource = new ABJ_404_Solution_TermCandidateSource(
76 $contentRepository,
77 $ngramFilterResolved instanceof ABJ_404_Solution_NGramFilter ? $ngramFilterResolved : null,
78 $publishedTermsProviderResolved instanceof ABJ_404_Solution_PublishedTermsProvider ? $publishedTermsProviderResolved : null,
79 $termCoveragePolicyResolved instanceof ABJ_404_Solution_TermNGramCoveragePolicy ? $termCoveragePolicyResolved : null
80 );
81 $this->suggestionScorer = new ABJ_404_Solution_SpellSuggestionScorer(
82 $functions, $logic, $logger, $contentRepository, $urlMatcher, $levenshteinEngine,
83 $separatingCharacters, $separatingCharactersForImages,
84 $termCandidateSource
85 );
86 }
87
88 /**
89 * Silent container lookup for an optional collaborator. Returns the
90 * registered service or null when the container or service is absent
91 * (e.g. unit tests that construct SpellCandidateFilter without bootstrap).
92 * The scorer degrades to the full taxonomy scan when these are null.
93 *
94 * @param string $serviceName
95 * @return mixed
96 */
97 private static function resolveOptionalService(string $serviceName) {
98 if (!class_exists('ABJ_404_Solution_ServiceContainer')) {
99 return null;
100 }
101 $container = ABJ_404_Solution_ServiceContainer::getInstance();
102 if (!$container->has($serviceName)) {
103 return null;
104 }
105 return $container->get($serviceName);
106 }
107
108 /**
109 * @param string $requestedURLRaw
110 * @param string $includeCats
111 * @param string $includeTags
112 * @return array<int, mixed>
113 */
114 function findMatchingPosts(string $requestedURLRaw, string $includeCats = '1', string $includeTags = '1') {
115
116 $options = abj_service('options_repository')->getOptions();
117 $excludePagesCount = 0;
118 $excludePagesRaw = isset($options['excludePages[]']) && is_string($options['excludePages[]']) ? $options['excludePages[]'] : '';
119 if (trim($excludePagesRaw) !== '') {
120 $jsonResult = json_decode($excludePagesRaw);
121 if (!is_array($jsonResult)) {
122 $jsonResult = array($jsonResult);
123 }
124 $excludePagesCount = count($jsonResult);
125 }
126 $suggestMaxRaw = isset($options['suggest_max']) && is_scalar($options['suggest_max']) ? $options['suggest_max'] : 5;
127 $maxCacheCount = absint($suggestMaxRaw) + $excludePagesCount;
128
129 $requestedURLSpaces = $this->f->str_replace($this->separatingCharacters, " ", $requestedURLRaw);
130 $requestedURLCleaned = $this->urlMatcher->getLastURLPart($requestedURLSpaces);
131 $fullURLspacesCleaned = $this->f->str_replace('/', " ", $requestedURLSpaces);
132 if ($fullURLspacesCleaned == $requestedURLCleaned) {
133 $fullURLspacesCleaned = '';
134 }
135
136 $this->postListeners->initializePublishedPostsProvider();
137 $this->levenshteinEngine->setPublishedPostsProvider(
138 $this->postListeners->getPublishedPostsProvider()
139 );
140
141 $rowType = 'pages';
142 $permalinks = array();
143 $permalinks = $this->matchOnPosts($permalinks, $requestedURLRaw, $requestedURLCleaned,
144 $fullURLspacesCleaned, $rowType);
145
146 if ($includeTags == "1") {
147 $permalinks = $this->matchOnTags($permalinks, $requestedURLCleaned, $fullURLspacesCleaned, 'tags');
148 }
149
150 if ($includeCats == "1") {
151 $permalinks = $this->matchOnCats($permalinks, $requestedURLCleaned, $fullURLspacesCleaned, 'categories');
152 }
153
154 $permalinks = $this->removeExcludedPages($options, $permalinks);
155
156 arsort($permalinks);
157
158 $permalinks = $this->removeExcludedPagesWithRegex($options, $permalinks, $maxCacheCount);
159
160 $permalinks = array_splice($permalinks, 0, $maxCacheCount);
161
162 $returnValue = array($permalinks,$rowType);
163 $this->contentRepository->storeSpellingPermalinksToCache($requestedURLRaw, $returnValue);
164 $ctx = abj_service('request_context');
165 $ctx->permalinks_found = (string)json_encode($returnValue);
166 $ctx->permalinks_kept = (string)json_encode($permalinks);
167
168 return $returnValue;
169 }
170
171 /**
172 * @param array<string, mixed> $options
173 * @param array<string, string> $permalinks
174 * @return array<string, string>
175 */
176 function removeExcludedPages(array $options, array $permalinks): array {
177 return $this->exclusionPolicy->removeExcludedPages($options, $permalinks);
178 }
179
180 /**
181 * @param array<string, mixed> $options
182 * @param array<string, string> $permalinks
183 * @param int $maxCacheCount
184 * @return array<string, string>
185 */
186 function removeExcludedPagesWithRegex(array $options, array $permalinks, int $maxCacheCount): array {
187 return $this->exclusionPolicy->removeExcludedPagesWithRegex($options, $permalinks, $maxCacheCount);
188 }
189
190 /**
191 * @param mixed $typeConstant
192 * @return string|null
193 */
194 public function mapTypeConstantToString($typeConstant) {
195 return $this->exclusionPolicy->mapTypeConstantToString($typeConstant);
196 }
197
198 /**
199 * @param array<string, string> $permalinks
200 * @param string $requestedURLCleaned
201 * @param string $fullURLspacesCleaned
202 * @param string $rowType
203 * @return array<string, string>
204 */
205 function matchOnCats(array $permalinks, string $requestedURLCleaned, string $fullURLspacesCleaned, string $rowType): array {
206 return $this->suggestionScorer->matchOnCats($permalinks, $requestedURLCleaned, $fullURLspacesCleaned);
207 }
208
209 /**
210 * @param array<string, string> $permalinks
211 * @param string $requestedURLCleaned
212 * @param string $fullURLspacesCleaned
213 * @param string $rowType
214 * @return array<string, string>
215 */
216 function matchOnTags(array $permalinks, string $requestedURLCleaned, string $fullURLspacesCleaned, string $rowType): array {
217 return $this->suggestionScorer->matchOnTags($permalinks, $requestedURLCleaned, $fullURLspacesCleaned);
218 }
219
220 /**
221 * @param array<string, string> $permalinks
222 * @param string $requestedURLRaw
223 * @param string $requestedURLCleaned
224 * @param string $fullURLspacesCleaned
225 * @param string $rowType
226 * @return array<string, string>
227 */
228 function matchOnPosts(array $permalinks, string $requestedURLRaw, string $requestedURLCleaned, string $fullURLspacesCleaned, string $rowType): array {
229 return $this->suggestionScorer->matchOnPosts($permalinks, $requestedURLRaw, $requestedURLCleaned, $fullURLspacesCleaned, $rowType);
230 }
231
232 }
233