Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 38 additions & 33 deletions app/Services/PluginServices/QueryPluginsService.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@ class QueryPluginsService
{
public function queryPlugins(Plugins\QueryPluginsRequest $req): Plugins\QueryPluginsResponse
{
$page = $req->page;
$page = $req->page;
$perPage = $req->per_page;
$browse = $req->browse ?: 'popular';
$search = $req->search ?? null;
$author = $req->author ?? null;
$browse = $req->browse ?: 'popular';
$search = $req->search ?? null;
$author = $req->author ?? null;

// Operators coming from the DTO
$tags = $req->tags ?? [];
$tags = $req->tags ?? [];
$tagAnd = $req->tagAnd ?? [];
$tagOr = $req->tagOr ?? [];
$tagOr = $req->tagOr ?? [];
$tagNot = $req->tagNot ?? [];

// merge base tags with tagOr
Expand All @@ -31,8 +31,8 @@ public function queryPlugins(Plugins\QueryPluginsRequest $req): Plugins\QueryPlu
$callbacks = collect();

!empty($anyTags) && $callbacks->push(fn($q) => self::applyTagAny($q, $anyTags));
!empty($tagAnd) && $callbacks->push(fn($q) => self::applyTagAll($q, $tagAnd));
!empty($tagNot) && $callbacks->push(fn($q) => self::applyTagNot($q, $tagNot));
!empty($tagAnd) && $callbacks->push(fn($q) => self::applyTagAll($q, $tagAnd));
!empty($tagNot) && $callbacks->push(fn($q) => self::applyTagNot($q, $tagNot));

$search && $callbacks->push(fn($q) => self::applySearchWeighted($q, $search, $req));
$author && $callbacks->push(fn($q) => self::applyAuthor($q, $author));
Expand All @@ -56,7 +56,7 @@ public function queryPlugins(Plugins\QueryPluginsRequest $req): Plugins\QueryPlu

return Plugins\QueryPluginsResponse::from([
'plugins' => $plugins,
'info' => ['page' => $page, 'pages' => $totalPages, 'results' => $total],
'info' => ['page' => $page, 'pages' => $totalPages, 'results' => $total],
]);
}

Expand All @@ -69,9 +69,8 @@ public function queryPlugins(Plugins\QueryPluginsRequest $req): Plugins\QueryPlu
public static function applySearchWeighted(
Builder $query,
string $search,
Plugins\QueryPluginsRequest $request
): Builder
{
Plugins\QueryPluginsRequest $request,
): Builder {
$lcsearch = mb_strtolower($search);
$slug = Regex::replace('/[^-\w]+/', '-', $lcsearch);
$wordchars = Regex::replace('/\W+/', '', $lcsearch);
Expand All @@ -84,7 +83,7 @@ public static function applySearchWeighted(
->orWhereRaw("slug %> ?", [$wordchars])
->orWhereRaw("name %> ?", [$wordchars])
->orWhereRaw("short_description %> ?", [$wordchars])
->orWhereFullText('description', $search)
->orWhereFullText('description', $search),
)
->selectRaw("plugins.*,
CASE
Expand All @@ -97,37 +96,36 @@ public static function applySearchWeighted(
WHEN short_description %> ? THEN 400000
WHEN to_tsvector('english', description) @@ plainto_tsquery(?) THEN 300000
ELSE 0
END + log(GREATEST($sortColumn, 1)) AS score", [
$search,
$search,
"$slug%",
"$search%",
$wordchars,
$wordchars,
$wordchars,
$search,
])
END + log(GREATEST($sortColumn, 1)) AS score",
[
$search,
$search,
"$slug%",
"$search%",
$wordchars,
$wordchars,
$wordchars,
$search,
])
->orderByDesc('score');
}

/** @param Builder<Plugin> $query */
public static function applyAuthor(Builder $query, string $author): Builder
{
return $query->where(fn(Builder $q)
=> $q
return $query->where(fn(Builder $q) => $q
->whereRaw("author %> '$author'")
->orWhereHas(
'contributors',
fn(Builder $q)
=> $q
fn(Builder $q) => $q
->whereRaw("user_nicename %> '$author'")
->orWhereRaw("display_name %> '$author'"),
));
}

/**
* @param Builder<Plugin> $query
* @param list<string> $tags
* @param list<string> $tags
*/
public static function applyTagAny(Builder $query, array $tags): Builder
{
Expand All @@ -136,21 +134,21 @@ public static function applyTagAny(Builder $query, array $tags): Builder

/**
* @param Builder<Plugin> $query
* @param list<string> $tags
* @param list<string> $tags
*/
public static function applyTagAll(Builder $query, array $tags): Builder
{
return $query->whereHas(
'tags',
fn(Builder $q) => $q->whereIn('slug', $tags),
'>=',
count($tags)
count($tags),
);
}

/**
* @param Builder<Plugin> $query
* @param list<string> $tags
* @param list<string> $tags
*/
public static function applyTagNot(Builder $query, array $tags): Builder
{
Expand All @@ -164,15 +162,22 @@ public static function applyTagNot(Builder $query, array $tags): Builder
*/
public static function applyBrowse(Builder $query, string $browse): Builder
{
if ($browse === 'featured') {
$query->where(fn($q) => $q
->where(fn($q) => $q->where('rating', '>=', 80)->where('num_ratings', '>', 100))
->orWhere('ac_origin', '!=', 'wp_org')
);
}

return $query->reorder(self::browseToSortColumn($browse), 'desc');
}

public static function browseToSortColumn(?string $browse): string
{
return match ($browse) {
'new' => 'added',
'updated' => 'last_updated',
'top-rated', 'featured' => 'rating',
'top-rated' => 'rating',
'updated', 'featured' => 'last_updated',
default => 'active_installs',
};
}
Expand Down
23 changes: 0 additions & 23 deletions tests/Feature/Services/Plugins/QueryPluginsServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -208,29 +208,6 @@
expect($sql)->toContain('order by');
});

test('browseToSortColumn returns correct column for each browse parameter', function () {
expect(QueryPluginsService::browseToSortColumn('new'))
->toBe('added')
->and(QueryPluginsService::browseToSortColumn('updated'))->toBe('last_updated')
->and(QueryPluginsService::browseToSortColumn('top-rated'))->toBe('rating')
->and(QueryPluginsService::browseToSortColumn('featured'))->toBe('rating')
->and(QueryPluginsService::browseToSortColumn('popular'))->toBe('active_installs')
->and(QueryPluginsService::browseToSortColumn(null))->toBe('active_installs');
});

// [chuck 2025-09-13] These are no longer used, but keeping them commented for future reference.
// If they're still not used after 6 months, just delete them.
//
// test('normalizeSearchString handles various inputs correctly', function () {
// expect(QueryPluginsService::normalizeSearchString(null))
// ->toBeNull()
// ->and(QueryPluginsService::normalizeSearchString(''))->toBe('')
// ->and(QueryPluginsService::normalizeSearchString(' test '))->toBe('test')
// ->and(QueryPluginsService::normalizeSearchString('test search'))->toBe('test search')
// ->and(QueryPluginsService::normalizeSearchString('[email protected]'))->toBe('[email protected]')
// ->and(QueryPluginsService::normalizeSearchString('test*search'))->toBe('test search');
// });

test('applySearchWeighted prioritizes relevance over install count', function () {
// Create plugins with different install counts and names
Plugin::factory()->create([
Expand Down
Loading