mirror of
https://ghproxy.net/https://github.com/aspirepress/AspireCloud.git
synced 2025-10-04 03:41:01 +08:00
Convert Resource Request/Response classes to Bag (#225)
* refactor: use Bag transformers for (Plugin|Theme)HotTagsResponse * refactor: replace more static constructors with Transforms * zap: rm unused ThemeUpdateCheckTranslationCollection (whew) VO * refactor: inline ThemeUpdateData::fromModelCollection * refactor: embaggify QueryPluginsRequest * refactor: Bag up PluginInformationRequest * refactor: throw PluginUpdateRequest into the Bag * test: move phpstan.neon -> phpstan.dist.neon * refactor: tighten up update check request types * test: use more realistic plugin filenames in update check tests * refactor: use single query for plugin update check * refactor: convert plugin updates to Bag (i'm out of "bag" neologisms) * refactor: use ThemeResponse VO for theme info requests * refactor: commit baggravated assault on ThemeCollection/ThemeResource ;) * refactor: bag up the rest of the Resource types * tweak: slipstream in dependabot upgrade
This commit is contained in:
parent
60f4feb4e5
commit
d9db849e11
47 changed files with 843 additions and 984 deletions
2
.github/workflows/run-checks.yaml
vendored
2
.github/workflows/run-checks.yaml
vendored
|
@ -15,7 +15,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Harden the runner (Audit all outbound calls)
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -22,9 +22,9 @@ yarn-error.log
|
|||
/.vscode
|
||||
/.zed
|
||||
|
||||
phpstan.neon
|
||||
|
||||
# helper files would not normally be ignored, but they're currently broken, so it's a local decision to use them
|
||||
_ide_helper.php
|
||||
.phpstorm.meta.php
|
||||
|
||||
grumphp.yml
|
||||
π
|
||||
|
|
|
@ -3,15 +3,14 @@
|
|||
namespace App\Http\Controllers\API\WpOrg\Plugins;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Plugins\PluginInformationRequest;
|
||||
use App\Http\Requests\Plugins\QueryPluginsRequest;
|
||||
use App\Http\Resources\Plugins\ClosedPluginResource;
|
||||
use App\Http\Resources\Plugins\PluginCollection;
|
||||
use App\Http\Resources\Plugins\PluginResource;
|
||||
use App\Models\WpOrg\ClosedPlugin;
|
||||
use App\Services\Plugins\PluginHotTagsService;
|
||||
use App\Services\Plugins\PluginInformationService;
|
||||
use App\Services\Plugins\QueryPluginsService;
|
||||
use App\Values\WpOrg\Plugins\ClosedPluginResponse;
|
||||
use App\Values\WpOrg\Plugins\PluginInformationRequest;
|
||||
use App\Values\WpOrg\Plugins\PluginResponse;
|
||||
use App\Values\WpOrg\Plugins\QueryPluginsRequest;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
|
@ -26,31 +25,26 @@ class PluginInformation_1_2_Controller extends Controller
|
|||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
return match ($request->query('action')) {
|
||||
'query_plugins' => $this->queryPlugins(new QueryPluginsRequest($request->all())),
|
||||
'plugin_information' => $this->pluginInformation(new PluginInformationRequest($request->all())),
|
||||
'query_plugins' => $this->queryPlugins(QueryPluginsRequest::from($request)),
|
||||
'plugin_information' => $this->pluginInformation(PluginInformationRequest::from($request)),
|
||||
'hot_tags', 'popular_tags' => $this->hotTags($request),
|
||||
default => response()->json(['error' => 'Invalid action'], 400),
|
||||
};
|
||||
}
|
||||
|
||||
private function pluginInformation(PluginInformationRequest $request): JsonResponse
|
||||
private function pluginInformation(PluginInformationRequest $req): JsonResponse
|
||||
{
|
||||
$slug = $request->getSlug();
|
||||
if (!$slug) {
|
||||
return response()->json(['error' => 'Slug is required'], 400);
|
||||
}
|
||||
|
||||
$plugin = $this->pluginInfo->findBySlug($request->getSlug());
|
||||
$plugin = $this->pluginInfo->findBySlug($req->slug);
|
||||
|
||||
if (!$plugin) {
|
||||
return response()->json(['error' => 'Plugin not found'], 404);
|
||||
}
|
||||
|
||||
if ($plugin instanceof ClosedPlugin) {
|
||||
$resource = new ClosedPluginResource($plugin);
|
||||
$resource = ClosedPluginResponse::from($plugin);
|
||||
$status = 404;
|
||||
} else {
|
||||
$resource = new PluginResource($plugin);
|
||||
$resource = PluginResponse::from($plugin)->asPluginInformationResponse();
|
||||
$status = 200;
|
||||
}
|
||||
return response()->json($resource, $status);
|
||||
|
@ -58,21 +52,8 @@ class PluginInformation_1_2_Controller extends Controller
|
|||
|
||||
private function queryPlugins(QueryPluginsRequest $request): JsonResponse
|
||||
{
|
||||
$result = $this->queryPlugins->queryPlugins(
|
||||
page: $request->getPage(),
|
||||
perPage: $request->getPerPage(),
|
||||
search: $request->query('search'),
|
||||
tag: $request->query('tag'),
|
||||
author: $request->query('author'),
|
||||
browse: $request->getBrowse(),
|
||||
);
|
||||
|
||||
return response()->json(new PluginCollection(
|
||||
PluginResource::collection($result['plugins']),
|
||||
$result['page'],
|
||||
$result['totalPages'],
|
||||
$result['total'],
|
||||
));
|
||||
$result = $this->queryPlugins->queryPlugins($request);
|
||||
return response()->json($result);
|
||||
}
|
||||
|
||||
private function hotTags(Request $request): JsonResponse
|
||||
|
|
|
@ -3,38 +3,25 @@
|
|||
namespace App\Http\Controllers\API\WpOrg\Plugins;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Plugins\PluginUpdateRequest;
|
||||
use App\Http\Resources\Plugins\PluginUpdateCollection;
|
||||
use App\Services\Plugins\PluginUpdateService;
|
||||
use App\Values\WpOrg\Plugins\PluginUpdateCheckRequest;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
use function Safe\json_decode;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PluginUpdateCheck_1_1_Controller extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PluginUpdateService $pluginService,
|
||||
private readonly PluginUpdateService $updateService,
|
||||
) {}
|
||||
|
||||
public function __invoke(PluginUpdateRequest $request): JsonResponse
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
$pluginsData = json_decode($request->plugins, true);
|
||||
// Bag's Laravel autoconversion doesn't work right, so do it by hand.
|
||||
$req = PluginUpdateCheckRequest::from($request);
|
||||
|
||||
$result = $this->pluginService->processPlugins(
|
||||
plugins: $pluginsData['plugins'],
|
||||
includeAll: $request->boolean('all'),
|
||||
);
|
||||
$result = $this->updateService->checkForUpdates($req);
|
||||
$req->all or $result = $result->withoutNoUpdate(); // we already generated the list, so just drop it
|
||||
|
||||
$response = [
|
||||
'plugins' => new PluginUpdateCollection($result['updates']),
|
||||
'translations' => [],
|
||||
];
|
||||
|
||||
// Only include no_update when 'all' parameter is true
|
||||
if ($request->boolean('all')) {
|
||||
$response['no_update'] = new PluginUpdateCollection($result['no_updates']);
|
||||
}
|
||||
|
||||
return response()->json($response);
|
||||
return response()->json($result);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,14 +4,14 @@ namespace App\Http\Controllers\API\WpOrg\Themes;
|
|||
|
||||
use App\Exceptions\NotFoundException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\ThemeCollection;
|
||||
use App\Http\Resources\ThemeResource;
|
||||
use App\Services\Themes\FeatureListService;
|
||||
use App\Services\Themes\QueryThemesService;
|
||||
use App\Services\Themes\ThemeHotTagsService;
|
||||
use App\Services\Themes\ThemeInformationService;
|
||||
use App\Values\WpOrg\Themes\QueryThemesRequest;
|
||||
use App\Values\WpOrg\Themes\QueryThemesResponse;
|
||||
use App\Values\WpOrg\Themes\ThemeInformationRequest;
|
||||
use App\Values\WpOrg\Themes\ThemeResponse;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
|
@ -49,7 +49,7 @@ class ThemeController extends Controller
|
|||
|
||||
private function doQueryThemes(Request $request): JsonResponse|Response
|
||||
{
|
||||
$req = QueryThemesRequest::fromRequest($request);
|
||||
$req = QueryThemesRequest::from($request);
|
||||
$themes = $this->queryThemes->queryThemes($req);
|
||||
return $this->sendResponse($themes);
|
||||
}
|
||||
|
@ -57,7 +57,8 @@ class ThemeController extends Controller
|
|||
private function doThemeInformation(Request $request): JsonResponse|Response
|
||||
{
|
||||
// NOTE: upstream requires slug query parameter to be request[slug], just slug is not recognized
|
||||
$response = $this->themeInfo->info(ThemeInformationRequest::fromRequest($request));
|
||||
$req = ThemeInformationRequest::fromRequest($request);
|
||||
$response = $this->themeInfo->info($req);
|
||||
return $this->sendResponse($response);
|
||||
}
|
||||
|
||||
|
@ -85,10 +86,10 @@ class ThemeController extends Controller
|
|||
/**
|
||||
* Send response based on API version.
|
||||
*
|
||||
* @param array<string,mixed>|ThemeCollection $response
|
||||
* @param array<string,mixed>|QueryThemesResponse|ThemeResponse $response
|
||||
*/
|
||||
private function sendResponse(
|
||||
array|ThemeCollection|ThemeResource $response,
|
||||
array|QueryThemesResponse|ThemeResponse $response,
|
||||
int $statusCode = 200,
|
||||
): JsonResponse|Response {
|
||||
$version = request()->route('version');
|
||||
|
|
|
@ -13,21 +13,18 @@ use Illuminate\Validation\ValidationException;
|
|||
|
||||
class ThemeUpdatesController extends Controller
|
||||
{
|
||||
/**
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function __invoke(Request $request): JsonResponse|Response
|
||||
{
|
||||
try {
|
||||
$updateRequest = ThemeUpdateCheckRequest::fromRequest($request);
|
||||
$req = ThemeUpdateCheckRequest::from($request);
|
||||
|
||||
$themes = Theme::query()
|
||||
->whereIn('slug', array_keys($updateRequest->themes))
|
||||
->whereIn('slug', array_keys($req->themes))
|
||||
->get()
|
||||
->partition(function ($theme) use ($updateRequest) {
|
||||
return version_compare($theme->version, $updateRequest->themes[$theme->slug]['Version'], '>');
|
||||
});
|
||||
return $this->sendResponse(ThemeUpdateCheckResponse::fromData($themes[0], $themes[1]));
|
||||
->partition(fn($theme) => version_compare($theme->version, $req->themes[$theme->slug]['Version'], '>'));
|
||||
|
||||
return $this->sendResponse(ThemeUpdateCheckResponse::fromResults($themes[0], $themes[1]));
|
||||
|
||||
} catch (ValidationException $e) {
|
||||
// Handle validation errors and return a custom response
|
||||
$firstErrorMessage = collect($e->errors())->flatten()->first();
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests\Plugins;
|
||||
|
||||
use Illuminate\Contracts\Validation\Validator;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||
|
||||
class PluginInformationRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, array<int, string>>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'slug' => ['required', 'string'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the plugin slug from the request
|
||||
*/
|
||||
public function getSlug(): ?string
|
||||
{
|
||||
return $this->query('slug');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a failed validation attempt.
|
||||
*/
|
||||
protected function failedValidation(Validator $validator): void
|
||||
{
|
||||
throw new HttpResponseException(
|
||||
response()->json([
|
||||
'error' => 'Slug is required',
|
||||
], 400),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests\Plugins;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
use function Safe\json_decode;
|
||||
|
||||
class PluginUpdateRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, array<int, string>>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'plugins' => ['required', 'string', 'json'],
|
||||
'translations' => ['required', 'string'],
|
||||
'locale' => ['required', 'string'],
|
||||
'all' => ['sometimes', 'string', 'in:true,false'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the validator instance.
|
||||
*/
|
||||
public function withValidator(Validator $validator): void
|
||||
{
|
||||
$validator->after(function ($validator) {
|
||||
if ($validator->failed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$plugins = json_decode($this->plugins, true);
|
||||
if (!isset($plugins['plugins']) || !is_array($plugins['plugins'])) {
|
||||
$validator->errors()->add(
|
||||
'plugins',
|
||||
'The plugins JSON must contain a "plugins" array.',
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests\Plugins;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class QueryPluginsRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, array<int, mixed>>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'page' => ['sometimes', 'integer', 'min:1'],
|
||||
'per_page' => ['sometimes', 'integer', 'min:1', 'max:100'],
|
||||
'search' => ['sometimes', 'string'],
|
||||
'tag' => ['sometimes', 'string'],
|
||||
'author' => ['sometimes', 'string'],
|
||||
'browse' => ['sometimes', 'string', 'in:new,updated,top-rated,popular'],
|
||||
];
|
||||
}
|
||||
|
||||
public function getPage(): int
|
||||
{
|
||||
return max(1, (int) $this->query('page', '1'));
|
||||
}
|
||||
|
||||
public function getPerPage(): int
|
||||
{
|
||||
return (int) $this->query('per_page', '24');
|
||||
}
|
||||
|
||||
public function getBrowse(): string
|
||||
{
|
||||
return $this->query('browse', 'popular');
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources\Plugins;
|
||||
|
||||
use App\Models\WpOrg\Plugin;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
abstract class BasePluginResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Get common plugin attributes that are shared across different contexts
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function getCommonAttributes(): array
|
||||
{
|
||||
$plugin = $this->resource;
|
||||
assert($plugin instanceof Plugin);
|
||||
|
||||
return [
|
||||
'name' => $plugin->name,
|
||||
'slug' => $plugin->slug,
|
||||
'version' => $plugin->version,
|
||||
'requires' => $plugin->requires,
|
||||
'tested' => $plugin->tested,
|
||||
'requires_php' => $plugin->requires_php,
|
||||
'download_link' => $plugin->download_link,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int[]|null $ratings
|
||||
* @return Collection<string, int>
|
||||
*/
|
||||
protected function mapRatings(?array $ratings): Collection
|
||||
{
|
||||
return collect($ratings ?? [])
|
||||
->mapWithKeys(fn($value, $key) => [(string) $key => $value]);
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources\Plugins;
|
||||
|
||||
use App\Models\WpOrg\ClosedPlugin;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ClosedPluginResource extends BasePluginResource
|
||||
{
|
||||
/** @return array<string, mixed> */
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
$plugin = $this->resource;
|
||||
assert($plugin instanceof ClosedPlugin);
|
||||
|
||||
return [
|
||||
'error' => 'closed',
|
||||
'name' => $plugin->name,
|
||||
'slug' => $plugin->slug,
|
||||
'description' => $plugin->description,
|
||||
'closed' => true,
|
||||
'closed_date' => $plugin->closed_date->format('Y-m-d'),
|
||||
'reason' => $plugin->reason,
|
||||
'reason_text' => $plugin->getReasonText(),
|
||||
];
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources\Plugins;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class PluginCollection extends ResourceCollection
|
||||
{
|
||||
private int $page;
|
||||
private int $pages;
|
||||
private int $results;
|
||||
|
||||
public function __construct($resource, int $page, int $pages, int $results)
|
||||
{
|
||||
parent::__construct($resource);
|
||||
$this->page = $page;
|
||||
$this->pages = $pages;
|
||||
$this->results = $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the resource collection into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'info' => [
|
||||
'page' => $this->page,
|
||||
'pages' => $this->pages,
|
||||
'results' => $this->results,
|
||||
],
|
||||
'plugins' => $this->collection,
|
||||
];
|
||||
}
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources\Plugins;
|
||||
|
||||
use App\Models\WpOrg\Plugin;
|
||||
use DateTimeInterface;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PluginResource extends BasePluginResource
|
||||
{
|
||||
public const LAST_UPDATED_DATE_FORMAT = 'Y-m-d h:ia T'; // .org's goofy format: "2024-09-27 9:53pm GMT"
|
||||
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
$plugin = $this->resource;
|
||||
assert($plugin instanceof Plugin);
|
||||
|
||||
$data = array_merge($this->getCommonAttributes(), [
|
||||
'author' => $plugin->author,
|
||||
'author_profile' => $plugin->author_profile,
|
||||
'rating' => $plugin->rating,
|
||||
'num_ratings' => $plugin->num_ratings,
|
||||
'ratings' => $this->mapRatings($plugin->ratings),
|
||||
'support_threads' => $plugin->support_threads,
|
||||
'support_threads_resolved' => $plugin->support_threads_resolved,
|
||||
'active_installs' => $plugin->active_installs,
|
||||
'last_updated' => self::formatLastUpdated($plugin->last_updated),
|
||||
'added' => $plugin->added?->format('Y-m-d'),
|
||||
'homepage' => $plugin->homepage,
|
||||
'tags' => $plugin->tagsArray(),
|
||||
'donate_link' => $plugin->donate_link,
|
||||
'requires_plugins' => $plugin->requires_plugins,
|
||||
]);
|
||||
|
||||
return match ($request->query('action')) {
|
||||
'query_plugins' => array_merge($data, [
|
||||
'downloaded' => $plugin->downloaded,
|
||||
'short_description' => $plugin->short_description,
|
||||
'description' => $plugin->description,
|
||||
'icons' => $plugin->icons,
|
||||
]),
|
||||
'plugin_information' => array_merge($data, [
|
||||
'sections' => $plugin->sections,
|
||||
'versions' => $plugin->versions,
|
||||
'contributors' => $plugin->contributors,
|
||||
'screenshots' => $plugin->screenshots,
|
||||
'support_url' => $plugin->support_url,
|
||||
'upgrade_notice' => $plugin->upgrade_notice,
|
||||
'business_model' => $plugin->business_model,
|
||||
'repository_url' => $plugin->repository_url,
|
||||
'commercial_support_url' => $plugin->commercial_support_url,
|
||||
'banners' => $plugin->banners,
|
||||
'preview_link' => $plugin->preview_link,
|
||||
]),
|
||||
default => $data,
|
||||
};
|
||||
}
|
||||
|
||||
private static function formatLastUpdated(?DateTimeInterface $lastUpdated): ?string
|
||||
{
|
||||
if ($lastUpdated === null) {
|
||||
return null;
|
||||
}
|
||||
$out = $lastUpdated->format(self::LAST_UPDATED_DATE_FORMAT);
|
||||
// Unfortunately this seems to render GMT as "GMT+0000" for some reason, so strip that out
|
||||
return \Safe\preg_replace('/\+\d+$/', '', $out);
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources\Plugins;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class PluginUpdateCollection extends ResourceCollection
|
||||
{
|
||||
/**
|
||||
* Transform the resource collection into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return $this->collection->all();
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources\Plugins;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PluginUpdateResource extends BasePluginResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => "w.org/plugins/{$this->resource['slug']}",
|
||||
'slug' => $this->resource['slug'],
|
||||
'plugin' => $this->resource['plugin'],
|
||||
'new_version' => $this->resource['new_version'],
|
||||
'url' => "https://wordpress.org/plugins/{$this->resource['slug']}/",
|
||||
'package' => $this->resource['package'],
|
||||
'icons' => $this->resource['icons'],
|
||||
'banners' => $this->resource['banners'],
|
||||
'banners_rtl' => $this->resource['banners_rtl'] ?? [],
|
||||
'requires' => $this->resource['requires'],
|
||||
'tested' => $this->resource['tested'],
|
||||
'requires_php' => $this->resource['requires_php'],
|
||||
'requires_plugins' => $this->resource['requires_plugins'] ?? [],
|
||||
'compatibility' => $this->resource['compatibility'] ?? [],
|
||||
];
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class ThemeCollection extends ResourceCollection
|
||||
{
|
||||
private int $page;
|
||||
private int $pages;
|
||||
private int $results;
|
||||
|
||||
public function __construct($resource, int $page, int $pages, int $results)
|
||||
{
|
||||
parent::__construct($resource);
|
||||
$this->page = $page;
|
||||
$this->pages = $pages;
|
||||
$this->results = $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the resource collection into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'info' => [
|
||||
'page' => $this->page,
|
||||
'pages' => $this->pages,
|
||||
'results' => $this->results,
|
||||
],
|
||||
'themes' => $this->collection,
|
||||
];
|
||||
}
|
||||
}
|
|
@ -1,118 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use App\Models\WpOrg\Author;
|
||||
use App\Models\WpOrg\Theme;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class ThemeResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array{
|
||||
* name: string,
|
||||
* slug: string,
|
||||
* version: string,
|
||||
* preview_url: string,
|
||||
* author: Author,
|
||||
* screenshot_url: string,
|
||||
* ratings: array{1:int, 2:int, 3:int, 4:int, 5:int},
|
||||
* rating: int,
|
||||
* num_ratings: int,
|
||||
* reviews_url: string,
|
||||
* downloaded: int,
|
||||
* active_installs: int,
|
||||
* last_updated: CarbonImmutable,
|
||||
* last_updated_time: CarbonImmutable,
|
||||
* creation_time: CarbonImmutable,
|
||||
* homepage: string,
|
||||
* sections: array<string, string>,
|
||||
* download_link: string,
|
||||
* tags: array<string, string>,
|
||||
* versions: array<string, string>,
|
||||
* requires: array<string, string>,
|
||||
* requires_php: string,
|
||||
* is_commercial: bool,
|
||||
* external_support_url: string|bool,
|
||||
* is_community: bool,
|
||||
* external_repository_url: string
|
||||
* }
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
$resource = $this->resource;
|
||||
assert($resource instanceof Theme);
|
||||
$author = $resource->author->toArray();
|
||||
unset($author['id']);
|
||||
|
||||
$tags = $resource->tagsArray();
|
||||
ksort($tags);
|
||||
|
||||
$screenshotBase = "https://wp-themes.com/wp-content/themes/{$resource->slug}/screenshot";
|
||||
return [
|
||||
'name' => $resource->name,
|
||||
'slug' => $resource->slug,
|
||||
'version' => $resource->version,
|
||||
'preview_url' => $resource->preview_url,
|
||||
'author' => $this->whenField('extended_author', $author, $resource->author->user_nicename),
|
||||
'description' => $this->whenField('description', fn() => $resource->description),
|
||||
'screenshot_url' => $this->whenField('screenshot_url', fn() => $resource->screenshot_url),
|
||||
// TODO: support screenshots metadata once I can track it down
|
||||
// 'screenshot_count' => $this->whenField('screenshot_count', fn() => max($resource->screenshot_count ?? 1, 1)),
|
||||
// 'screenshots' => $this->whenField('screenshots', function () use ($screenshotBase) {
|
||||
// $screenshotCount = max($resource->screenshot_count ?? 1, 1);
|
||||
// return collect(range(1, $screenshotCount))->map(fn($i) => "{$screenshotBase}-{$i}.png");
|
||||
// }),
|
||||
'ratings' => $this->whenField('ratings', fn() => (object) $resource->ratings), // need the object cast when all keys are numeric
|
||||
'rating' => $this->whenField('rating', fn() => $resource->rating),
|
||||
'num_ratings' => $this->whenField('rating', fn() => $resource->num_ratings),
|
||||
'reviews_url' => $this->whenField('reviews_url', $resource->reviews_url),
|
||||
'downloaded' => $this->whenField('downloaded', fn() => $resource->downloaded),
|
||||
'active_installs' => $this->whenField('active_installs', $resource->active_installs),
|
||||
'last_updated' => $this->whenField('last_updated', fn() => $resource->last_updated->format('Y-m-d')),
|
||||
'last_updated_time' => $this->whenField('last_updated', fn() => $resource->last_updated->format('Y-m-d H:i:s')),
|
||||
'creation_time' => $this->whenField('creation_time', fn() => $resource->creation_time->format('Y-m-d H:i:s')),
|
||||
'homepage' => $this->whenField('homepage', fn() => "https://wordpress.org/themes/{$resource->slug}/"),
|
||||
'sections' => $this->whenField('sections', fn() => $resource->sections),
|
||||
'download_link' => $this->whenField('downloadlink', fn() => $resource->download_link ?? ''),
|
||||
'tags' => $this->whenField('tags', fn() => $tags),
|
||||
'versions' => $this->whenField('versions', fn() => $resource->versions),
|
||||
// TODO: support parent
|
||||
// 'parent' => $this->whenField('parent', function () use ($resource) {
|
||||
// $parent = $resource->parent_theme;
|
||||
// return $parent ? [
|
||||
// 'slug' => $parent->slug,
|
||||
// 'name' => $parent->name,
|
||||
// 'homepage' => "https://wordpress.org/themes/{$parent->slug}/",
|
||||
// ] : new MissingValue();
|
||||
// }),
|
||||
'requires' => $this->whenField('requires', $resource->requires),
|
||||
'requires_php' => $this->whenField('requires_php', $resource->requires_php),
|
||||
'is_commercial' => $this->whenField('is_commercial', fn() => $resource->is_commercial),
|
||||
'external_support_url' => $this->whenField('external_support_url', fn() => $resource->is_commercial ? $resource->external_support_url : false),
|
||||
'is_community' => $this->whenField('is_community', fn() => $resource->is_community),
|
||||
'external_repository_url' => $this->whenField('external_repository_url', fn() => $resource->is_community ? $resource->external_repository_url : ''),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* When the given field is included, the value is returned.
|
||||
* Otherwise, the default value is returned.
|
||||
* @param mixed $value
|
||||
* @param mixed|null $default
|
||||
*/
|
||||
private function whenField(string $fieldName, $value, $default = null): mixed
|
||||
{
|
||||
$include = false;
|
||||
$includedFields = $this->additional['fields'] ?? [];
|
||||
$include = $includedFields[$fieldName] ?? false;
|
||||
if (func_num_args() === 3) {
|
||||
return $this->when($include, $value, $default);
|
||||
}
|
||||
return $this->when($include, $value);
|
||||
}
|
||||
}
|
|
@ -10,24 +10,18 @@ class PluginHotTagsService
|
|||
/**
|
||||
* Gets the top tags by plugin count
|
||||
*
|
||||
* @return array<string, array{
|
||||
* name: string,
|
||||
* slug: string,
|
||||
* count: int,
|
||||
* }> */
|
||||
* @return array<string, array{ name: string, slug: string, count: int }>
|
||||
*/
|
||||
public function getHotTags(int $count = -1): array
|
||||
{
|
||||
$hotTags = PluginTag::withCount('plugins')
|
||||
$hotTags = PluginTag::query()
|
||||
->withCount('plugins')
|
||||
->orderBy('plugins_count', 'desc')
|
||||
->limit($count >= 0 ? $count : 100)
|
||||
->get(['slug', 'name', 'plugins_count'])
|
||||
->map(function ($tag) {
|
||||
return [
|
||||
'name' => (string) $tag->name,
|
||||
'slug' => (string) $tag->slug,
|
||||
'count' => (int) $tag->plugins_count,
|
||||
];
|
||||
});
|
||||
return PluginHotTagsResponse::fromCollection($hotTags)->toArray();
|
||||
->get();
|
||||
|
||||
return PluginHotTagsResponse::collect($hotTags)
|
||||
->mapWithKeys(fn(PluginHotTagsResponse $tag) => [$tag->slug => $tag])
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,62 +3,33 @@
|
|||
namespace App\Services\Plugins;
|
||||
|
||||
use App\Models\WpOrg\Plugin;
|
||||
use Illuminate\Support\Collection;
|
||||
use App\Values\WpOrg\Plugins\PluginUpdateCheckRequest;
|
||||
use App\Values\WpOrg\Plugins\PluginUpdateCheckResponse;
|
||||
use App\Values\WpOrg\Plugins\PluginUpdateData;
|
||||
|
||||
class PluginUpdateService
|
||||
{
|
||||
/**
|
||||
* Process the plugins and check for updates
|
||||
*
|
||||
* @param array<string, array{Version?: string}> $plugins
|
||||
* @return array{
|
||||
* updates: Collection<string, array<string, mixed>>,
|
||||
* no_updates: Collection<string, array<string, mixed>>
|
||||
* }
|
||||
*/
|
||||
public function processPlugins(array $plugins, bool $includeAll): array
|
||||
public function checkForUpdates(PluginUpdateCheckRequest $req): PluginUpdateCheckResponse
|
||||
{
|
||||
$updates = collect();
|
||||
$noUpdates = collect();
|
||||
$bySlug = collect($req->plugins)
|
||||
->mapWithKeys(
|
||||
fn($pluginData, $pluginFile) => [$this->extractSlug($pluginFile) => [$pluginFile, $pluginData]],
|
||||
);
|
||||
|
||||
foreach ($plugins as $pluginFile => $pluginData) {
|
||||
$plugin = $this->findPlugin($pluginFile, $pluginData);
|
||||
$isUpdated = fn($plugin) => version_compare($plugin->version, $bySlug[$plugin->slug][1]['Version'] ?? '', '>');
|
||||
|
||||
if (!$plugin) {
|
||||
continue;
|
||||
}
|
||||
$mkUpdate = function ($plugin) use ($bySlug) {
|
||||
$file = $bySlug[$plugin->slug][0];
|
||||
return [$file => PluginUpdateData::from($plugin)->with(plugin: $file)];
|
||||
};
|
||||
|
||||
$updateData = $this->formatPluginData($plugin, $pluginFile);
|
||||
[$updates, $no_updates] = Plugin::query()
|
||||
->whereIn('slug', $bySlug->keys())
|
||||
->get()
|
||||
->partition($isUpdated)
|
||||
->map(fn($collection) => $collection->mapWithKeys($mkUpdate));
|
||||
|
||||
if (version_compare($plugin->version, $pluginData['Version'] ?? '', '>')) {
|
||||
$updates->put($pluginFile, $updateData);
|
||||
} elseif ($includeAll) {
|
||||
// Only collect no_updates when includeAll is true
|
||||
$noUpdates->put($pluginFile, $updateData);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'updates' => $updates,
|
||||
'no_updates' => $noUpdates,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a plugin by its file path and data
|
||||
*
|
||||
* @param array{Version?: string} $pluginData
|
||||
*/
|
||||
private function findPlugin(string $pluginFile, array $pluginData): ?Plugin
|
||||
{
|
||||
$slug = $this->extractSlug($pluginFile);
|
||||
if (!$slug || empty($pluginData['Version'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Plugin::query()
|
||||
->where('slug', $slug)
|
||||
->first();
|
||||
return PluginUpdateCheckResponse::from(plugins: $updates, no_update: $no_updates, translations: collect([]));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -70,29 +41,4 @@ class PluginUpdateService
|
|||
? explode('/', $pluginFile)[0]
|
||||
: pathinfo($pluginFile, PATHINFO_FILENAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format plugin data for the response
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function formatPluginData(Plugin $plugin, string $pluginFile): array
|
||||
{
|
||||
return [
|
||||
'id' => "w.org/plugins/{$plugin->slug}",
|
||||
'slug' => $plugin->slug,
|
||||
'plugin' => $pluginFile,
|
||||
'new_version' => $plugin->version,
|
||||
'url' => "https://wordpress.org/plugins/{$plugin->slug}/",
|
||||
'package' => $plugin->download_link,
|
||||
'icons' => $plugin->icons,
|
||||
'banners' => $plugin->banners,
|
||||
'banners_rtl' => [],
|
||||
'requires' => $plugin->requires,
|
||||
'tested' => $plugin->tested,
|
||||
'requires_php' => $plugin->requires_php,
|
||||
'requires_plugins' => $plugin->requires_plugins,
|
||||
'compatibility' => $plugin->compatibility,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,29 +4,25 @@ namespace App\Services\Plugins;
|
|||
|
||||
use App\Models\WpOrg\Plugin;
|
||||
use App\Utils\Regex;
|
||||
use App\Values\WpOrg\Plugins\PluginResponse;
|
||||
use App\Values\WpOrg\Plugins\QueryPluginsRequest;
|
||||
use App\Values\WpOrg\Plugins\QueryPluginsResponse;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
class QueryPluginsService
|
||||
{
|
||||
/**
|
||||
* Query plugins with filters and pagination
|
||||
*
|
||||
* @return array{
|
||||
* plugins: Collection<int, Plugin>,
|
||||
* page: int,
|
||||
* totalPages: int,
|
||||
* total: int
|
||||
* }
|
||||
*/
|
||||
public function queryPlugins(
|
||||
int $page,
|
||||
int $perPage,
|
||||
?string $search = null,
|
||||
?string $tag = null, // TODO: make this work with more than one tag, the way Themes do
|
||||
?string $author = null,
|
||||
string $browse = 'popular',
|
||||
): array {
|
||||
public function queryPlugins(QueryPluginsRequest $req): QueryPluginsResponse
|
||||
{
|
||||
$page = $req->page;
|
||||
$perPage = $req->per_page;
|
||||
$browse = $req->browse ?: 'popular';
|
||||
$search = $req->search;
|
||||
$tag = $req->tags[0] ?? null; // TODO: multiple tags support
|
||||
$author = $req->author;
|
||||
|
||||
$search = self::normalizeSearchString($search);
|
||||
$tag = self::normalizeSearchString($tag);
|
||||
$author = self::normalizeSearchString($author);
|
||||
|
@ -38,20 +34,19 @@ class QueryPluginsService
|
|||
->when($author, self::applyAuthor(...));
|
||||
|
||||
$total = $query->count();
|
||||
$totalPages = (int) ceil($total / $perPage);
|
||||
$totalPages = (int)ceil($total / $perPage);
|
||||
|
||||
$plugins = $query
|
||||
->offset(($page - 1) * $perPage)
|
||||
->limit($perPage)
|
||||
->get()
|
||||
->unique('slug');
|
||||
->unique('slug')
|
||||
->map(fn($plugin) => PluginResponse::from($plugin)->asQueryPluginsResponse());
|
||||
|
||||
return [
|
||||
return QueryPluginsResponse::from([
|
||||
'plugins' => $plugins,
|
||||
'page' => $page,
|
||||
'totalPages' => $totalPages,
|
||||
'total' => $total,
|
||||
];
|
||||
'info' => ['page' => $page, 'pages' => $totalPages, 'results' => $total],
|
||||
]);
|
||||
}
|
||||
|
||||
/** @param Builder<Plugin> $query */
|
||||
|
|
|
@ -2,16 +2,16 @@
|
|||
|
||||
namespace App\Services\Themes;
|
||||
|
||||
use App\Http\Resources\ThemeCollection;
|
||||
use App\Http\Resources\ThemeResource;
|
||||
use App\Models\WpOrg\Theme;
|
||||
use App\Utils\Regex;
|
||||
use App\Values\WpOrg\Themes\QueryThemesRequest;
|
||||
use App\Values\WpOrg\Themes\QueryThemesResponse;
|
||||
use App\Values\WpOrg\Themes\ThemeResponse;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class QueryThemesService
|
||||
{
|
||||
public function queryThemes(QueryThemesRequest $req): ThemeCollection
|
||||
public function queryThemes(QueryThemesRequest $req): QueryThemesResponse
|
||||
{
|
||||
$page = $req->page;
|
||||
$perPage = $req->per_page;
|
||||
|
@ -36,11 +36,12 @@ class QueryThemesService
|
|||
->with('author')
|
||||
->get();
|
||||
|
||||
$collection = collect($themes)
|
||||
->unique('slug')
|
||||
->map(fn($theme) => (new ThemeResource($theme))->additional(['fields' => $req->fields]));
|
||||
$collection = ThemeResponse::collect($themes)->map(fn($theme) => $theme->withFields($req->fields ?? []));
|
||||
|
||||
return new ThemeCollection($collection, $page, (int) ceil($total / $perPage), $total);
|
||||
return QueryThemesResponse::from(
|
||||
themes: $collection,
|
||||
info: ['page' => $page, 'pages' => (int)ceil($total / $perPage), 'results' => $total],
|
||||
);
|
||||
}
|
||||
|
||||
/** @param Builder<Theme> $query */
|
||||
|
|
|
@ -10,24 +10,18 @@ class ThemeHotTagsService
|
|||
/**
|
||||
* Gets the top tags by theme count
|
||||
*
|
||||
* @return array<string, array{
|
||||
* name: string,
|
||||
* slug: string,
|
||||
* count: int,
|
||||
* }> */
|
||||
* @return array<string, array{name: string, slug: string, count: int}>
|
||||
*/
|
||||
public function getHotTags(int $count = -1): array
|
||||
{
|
||||
$hotTags = ThemeTag::withCount('themes')
|
||||
$hotTags = ThemeTag::query()
|
||||
->withCount('themes')
|
||||
->orderBy('themes_count', 'desc')
|
||||
->limit($count >= 0 ? $count : 100)
|
||||
->get(['slug', 'name', 'themes_count'])
|
||||
->map(function ($tag) {
|
||||
return [
|
||||
'name' => (string) $tag->name,
|
||||
'slug' => (string) $tag->slug,
|
||||
'count' => (int) $tag->themes_count,
|
||||
];
|
||||
});
|
||||
return ThemeHotTagsResponse::fromCollection($hotTags)->toArray();
|
||||
->get();
|
||||
|
||||
return ThemeHotTagsResponse::collect($hotTags)
|
||||
->mapWithKeys(fn(ThemeHotTagsResponse $tag) => [$tag->slug => $tag])
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,15 +3,15 @@
|
|||
namespace App\Services\Themes;
|
||||
|
||||
use App\Exceptions\NotFoundException;
|
||||
use App\Http\Resources\ThemeResource;
|
||||
use App\Models\WpOrg\Theme;
|
||||
use App\Values\WpOrg\Themes\ThemeInformationRequest;
|
||||
use App\Values\WpOrg\Themes\ThemeResponse;
|
||||
|
||||
class ThemeInformationService
|
||||
{
|
||||
public function info(ThemeInformationRequest $request): ThemeResource
|
||||
public function info(ThemeInformationRequest $req): ThemeResponse
|
||||
{
|
||||
$theme = Theme::query()->where('slug', $request->slug)->first() or throw new NotFoundException("Theme not found");
|
||||
return (new ThemeResource($theme))->additional(['fields' => $request->fields]);
|
||||
$theme = Theme::query()->where('slug', $req->slug)->first() or throw new NotFoundException("Theme not found");
|
||||
return ThemeResponse::from($theme)->withFields($req->fields ?? []);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,16 +2,34 @@
|
|||
|
||||
namespace App\Values\WpOrg;
|
||||
|
||||
use App\Models\WpOrg\Author as AuthorModel;
|
||||
use Bag\Attributes\Transforms;
|
||||
use Bag\Bag;
|
||||
|
||||
readonly class Author extends Bag
|
||||
{
|
||||
public function __construct(
|
||||
public string $user_nicename,
|
||||
public string $profile,
|
||||
public string $avatar,
|
||||
public string $display_name,
|
||||
public string $author,
|
||||
public string $author_url,
|
||||
public string|null $profile,
|
||||
public string|null $avatar,
|
||||
public string|null $display_name,
|
||||
public string|null $author,
|
||||
public string|null $author_url,
|
||||
) {}
|
||||
|
||||
// I wish I didn't have to write this, but alas it serializes $model to json inside $user_nicename otherwise 🤦
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
#[Transforms(AuthorModel::class)]
|
||||
public static function fromModel(AuthorModel $model): array
|
||||
{
|
||||
return [
|
||||
'user_nicename' => $model->user_nicename,
|
||||
'profile' => $model->profile,
|
||||
'avatar' => $model->avatar,
|
||||
'display_name' => $model->display_name,
|
||||
'author' => $model->author,
|
||||
'author_url' => $model->author_url,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
37
app/Values/WpOrg/Plugins/ClosedPluginResponse.php
Normal file
37
app/Values/WpOrg/Plugins/ClosedPluginResponse.php
Normal file
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Values\WpOrg\Plugins;
|
||||
|
||||
use App\Models\WpOrg\ClosedPlugin;
|
||||
use Bag\Attributes\Transforms;
|
||||
use Bag\Bag;
|
||||
|
||||
readonly class ClosedPluginResponse extends Bag
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public string $slug,
|
||||
public string $description,
|
||||
public string $closed_date,
|
||||
public string $reason,
|
||||
public string $reason_text,
|
||||
public string $error = 'closed',
|
||||
public bool $closed = true,
|
||||
) {}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
#[Transforms(ClosedPlugin::class)]
|
||||
public static function fromClosedPlugin(ClosedPlugin $plugin): array
|
||||
{
|
||||
return [
|
||||
'name' => $plugin->name,
|
||||
'slug' => $plugin->slug,
|
||||
'description' => $plugin->description,
|
||||
'closed_date' => $plugin->closed_date->format('Y-m-d'),
|
||||
'reason' => $plugin->reason,
|
||||
'reason_text' => $plugin->getReasonText(),
|
||||
];
|
||||
}
|
||||
}
|
|
@ -2,8 +2,9 @@
|
|||
|
||||
namespace App\Values\WpOrg\Plugins;
|
||||
|
||||
use App\Models\WpOrg\PluginTag;
|
||||
use Bag\Attributes\Transforms;
|
||||
use Bag\Bag;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
readonly class PluginHotTagsResponse extends Bag
|
||||
{
|
||||
|
@ -13,21 +14,10 @@ readonly class PluginHotTagsResponse extends Bag
|
|||
public int $count,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Static method to create an instance from a Plugin model.
|
||||
*
|
||||
* @param Collection<int,covariant array{
|
||||
* slug: string,
|
||||
* name: string,
|
||||
* count: int,
|
||||
* }> $pluginTags
|
||||
* @return Collection<string, covariant PluginHotTagsResponse>
|
||||
*/
|
||||
public static function fromCollection(Collection $pluginTags): Collection
|
||||
/** @return array{slug: string, name: string, count: int} */
|
||||
#[Transforms(PluginTag::class)]
|
||||
public static function fromPluginTag(PluginTag $tag): array
|
||||
{
|
||||
return $pluginTags->mapWithKeys(fn($plugin)
|
||||
=> [
|
||||
$plugin['slug'] => self::from($plugin),
|
||||
]);
|
||||
return ['slug' => $tag->slug, 'name' => $tag->name, 'count' => $tag->plugins_count];
|
||||
}
|
||||
}
|
||||
|
|
25
app/Values/WpOrg/Plugins/PluginInformationRequest.php
Normal file
25
app/Values/WpOrg/Plugins/PluginInformationRequest.php
Normal file
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace App\Values\WpOrg\Plugins;
|
||||
|
||||
use Bag\Attributes\StripExtraParameters;
|
||||
use Bag\Attributes\Transforms;
|
||||
use Bag\Bag;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
// Far simpler than ThemeInformationRequest, it takes a slug and that's it
|
||||
#[StripExtraParameters]
|
||||
readonly class PluginInformationRequest extends Bag
|
||||
{
|
||||
public const ACTION = 'plugin_information';
|
||||
|
||||
public function __construct(public string $slug) {}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
#[Transforms(Request::class)]
|
||||
public static function fromRequest(Request $request): array
|
||||
{
|
||||
// Bag throws 500 (RuntimeException) for missing fields, this throws a friendlier 422
|
||||
return $request->validate(['slug' => 'required']);
|
||||
}
|
||||
}
|
162
app/Values/WpOrg/Plugins/PluginResponse.php
Normal file
162
app/Values/WpOrg/Plugins/PluginResponse.php
Normal file
|
@ -0,0 +1,162 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Values\WpOrg\Plugins;
|
||||
|
||||
use App\Models\WpOrg\Plugin;
|
||||
use Bag\Attributes\Transforms;
|
||||
use Bag\Bag;
|
||||
use Bag\Values\Optional;
|
||||
use DateTimeInterface;
|
||||
|
||||
readonly class PluginResponse extends Bag
|
||||
{
|
||||
public const LAST_UPDATED_DATE_FORMAT = 'Y-m-d h:ia T'; // .org's goofy format: "2024-09-27 9:53pm GMT"
|
||||
|
||||
/**
|
||||
* @param array<array-key, mixed> $banners
|
||||
* @param array<array-key, array{src: string, caption: string}> $screenshots
|
||||
* @param array<string, mixed> $contributors
|
||||
* @param array<string, string> $versions
|
||||
* @param array<string, string> $sections
|
||||
* @param array{"1":int, "2":int, "3":int, "4":int, "5":int} $ratings
|
||||
* @param list<string> $requires_plugins
|
||||
* @param array<string, string> $icons
|
||||
* @param array<string, string> $upgrade_notice
|
||||
* @param array<string, string> $tags
|
||||
*/
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public string $slug,
|
||||
public string $version,
|
||||
public string|null $requires,
|
||||
public string|null $tested,
|
||||
public string|null $requires_php,
|
||||
public string $download_link,
|
||||
public string $author,
|
||||
public string|null $author_profile,
|
||||
public int $rating,
|
||||
public int $num_ratings,
|
||||
public array $ratings,
|
||||
public int $support_threads,
|
||||
public int $support_threads_resolved,
|
||||
public int $active_installs,
|
||||
public string|null $last_updated,
|
||||
public string|null $added,
|
||||
public string|null $homepage,
|
||||
public array $tags,
|
||||
public string|null $donate_link,
|
||||
public array $requires_plugins,
|
||||
//
|
||||
// // query_plugins only
|
||||
public Optional|string|null $downloaded,
|
||||
public Optional|string|null $short_description,
|
||||
public Optional|string|null $description,
|
||||
public Optional|array $icons,
|
||||
//
|
||||
// // plugin_information only
|
||||
public Optional|array $sections,
|
||||
public Optional|array $versions,
|
||||
public Optional|array $contributors,
|
||||
public Optional|array $screenshots,
|
||||
public Optional|string|null $support_url,
|
||||
public Optional|array|null $upgrade_notice,
|
||||
public Optional|string|null $business_model,
|
||||
public Optional|string|null $repository_url,
|
||||
public Optional|string|null $commercial_support_url,
|
||||
public Optional|array $banners,
|
||||
public Optional|string|null $preview_link,
|
||||
) {}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
#[Transforms(Plugin::class)]
|
||||
public static function fromPlugin(Plugin $plugin): array
|
||||
{
|
||||
$none = new Optional();
|
||||
|
||||
return [
|
||||
// common
|
||||
'name' => $plugin->name,
|
||||
'slug' => $plugin->slug,
|
||||
'version' => $plugin->version,
|
||||
'requires' => $plugin->requires,
|
||||
'tested' => $plugin->tested,
|
||||
'requires_php' => $plugin->requires_php,
|
||||
'download_link' => $plugin->download_link,
|
||||
'author' => $plugin->author,
|
||||
'author_profile' => $plugin->author_profile,
|
||||
'rating' => $plugin->rating,
|
||||
'num_ratings' => $plugin->num_ratings,
|
||||
'ratings' => $plugin->ratings,
|
||||
'support_threads' => $plugin->support_threads,
|
||||
'support_threads_resolved' => $plugin->support_threads_resolved,
|
||||
'active_installs' => $plugin->active_installs,
|
||||
'last_updated' => self::formatLastUpdated($plugin->last_updated),
|
||||
'added' => $plugin->added?->format('Y-m-d'),
|
||||
'homepage' => $plugin->homepage,
|
||||
'tags' => $plugin->tagsArray(),
|
||||
'donate_link' => $plugin->donate_link,
|
||||
'requires_plugins' => $plugin->requires_plugins,
|
||||
|
||||
// query_plugins only
|
||||
'downloaded' => $plugin->downloaded,
|
||||
'short_description' => $plugin->short_description,
|
||||
'description' => $plugin->description,
|
||||
'icons' => $plugin->icons,
|
||||
|
||||
// plugin_information only
|
||||
'sections' => $plugin->sections,
|
||||
'versions' => $plugin->versions,
|
||||
'contributors' => $plugin->contributors,
|
||||
'screenshots' => $plugin->screenshots,
|
||||
'support_url' => $plugin->support_url,
|
||||
'upgrade_notice' => $plugin->upgrade_notice ?: $none,
|
||||
'business_model' => $plugin->business_model,
|
||||
'repository_url' => $plugin->repository_url,
|
||||
'commercial_support_url' => $plugin->commercial_support_url,
|
||||
'banners' => $plugin->banners,
|
||||
'preview_link' => $plugin->preview_link,
|
||||
|
||||
];
|
||||
}
|
||||
|
||||
public function asQueryPluginsResponse(): static
|
||||
{
|
||||
$none = new Optional();
|
||||
return $this->with([
|
||||
'sections' => $none,
|
||||
'versions' => $none,
|
||||
'contributors' => $none,
|
||||
'screenshots' => $none,
|
||||
'support_url' => $none,
|
||||
'upgrade_notice' => $none,
|
||||
'business_model' => $none,
|
||||
'repository_url' => $none,
|
||||
'commercial_support_url' => $none,
|
||||
'banners' => $none,
|
||||
'preview_link' => $none,
|
||||
]);
|
||||
}
|
||||
|
||||
public function asPluginInformationResponse(): static
|
||||
{
|
||||
$none = new Optional();
|
||||
return $this->with([
|
||||
'downloaded' => $none,
|
||||
'short_description' => $none,
|
||||
'description' => $none,
|
||||
'icons' => $none,
|
||||
]);
|
||||
}
|
||||
|
||||
private static function formatLastUpdated(?DateTimeInterface $lastUpdated): ?string
|
||||
{
|
||||
if ($lastUpdated === null) {
|
||||
return null;
|
||||
}
|
||||
$out = $lastUpdated->format(self::LAST_UPDATED_DATE_FORMAT);
|
||||
// Unfortunately this seems to render GMT as "GMT+0000" for some reason, so strip that out
|
||||
return \Safe\preg_replace('/\+\d+$/', '', $out);
|
||||
}
|
||||
}
|
45
app/Values/WpOrg/Plugins/PluginUpdateCheckRequest.php
Normal file
45
app/Values/WpOrg/Plugins/PluginUpdateCheckRequest.php
Normal file
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
namespace App\Values\WpOrg\Plugins;
|
||||
|
||||
use Bag\Attributes\Transforms;
|
||||
use Bag\Bag;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use function Safe\json_decode;
|
||||
|
||||
/**
|
||||
* @phpstan-type TranslationMetadata array{
|
||||
* POT-Creation-Date: string,
|
||||
* PO-Revision-Date: string,
|
||||
* Project-Id-Version: string,
|
||||
* X-Generator: string
|
||||
* }
|
||||
*/
|
||||
readonly class PluginUpdateCheckRequest extends Bag
|
||||
{
|
||||
/**
|
||||
* @param array<string, array{"Version": string}> $plugins
|
||||
* @param array<string, array<string, TranslationMetadata>> $translations
|
||||
* @param list<string> $locale
|
||||
*/
|
||||
public function __construct(
|
||||
public array $plugins,
|
||||
public array $translations,
|
||||
public array $locale,
|
||||
public bool $all = false,
|
||||
) {}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
#[Transforms(Request::class)]
|
||||
public static function fromRequest(Request $request): array
|
||||
{
|
||||
$decode = fn($key) => json_decode($request->post($key), true);
|
||||
return [
|
||||
'plugins' => $decode('plugins')['plugins'],
|
||||
'locale' => $decode('locale'),
|
||||
'translations' => $decode('translations'),
|
||||
'all' => $request->boolean('all'),
|
||||
];
|
||||
}
|
||||
}
|
27
app/Values/WpOrg/Plugins/PluginUpdateCheckResponse.php
Normal file
27
app/Values/WpOrg/Plugins/PluginUpdateCheckResponse.php
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace App\Values\WpOrg\Plugins;
|
||||
|
||||
use Bag\Bag;
|
||||
use Bag\Values\Optional;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
readonly class PluginUpdateCheckResponse extends Bag
|
||||
{
|
||||
/**
|
||||
* @param Collection<string, PluginUpdateData> $plugins
|
||||
* @param Optional|Collection<string, PluginUpdateData> $no_update
|
||||
* @param Collection<array-key, mixed> $translations
|
||||
*/
|
||||
public function __construct(
|
||||
public Collection $plugins,
|
||||
public Optional|Collection $no_update,
|
||||
public Collection $translations,
|
||||
) {}
|
||||
|
||||
// not the best name for the method but that's what we get for a negative-named property. $no_tea anyone?
|
||||
public function withoutNoUpdate(): self
|
||||
{
|
||||
return $this->with(no_update: new Optional());
|
||||
}
|
||||
}
|
60
app/Values/WpOrg/Plugins/PluginUpdateData.php
Normal file
60
app/Values/WpOrg/Plugins/PluginUpdateData.php
Normal file
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
namespace App\Values\WpOrg\Plugins;
|
||||
|
||||
use App\Models\WpOrg\Plugin;
|
||||
use Bag\Attributes\Transforms;
|
||||
use Bag\Bag;
|
||||
use Bag\Values\Optional;
|
||||
|
||||
readonly class PluginUpdateData extends Bag
|
||||
{
|
||||
/**
|
||||
* @param Optional|list<string> $requires_plugins
|
||||
* @param Optional|array<string, mixed> $compatibility
|
||||
* @param Optional|array<string, mixed> $icons
|
||||
* @param Optional|array<string, mixed> $banners
|
||||
* @param Optional|array<string, mixed> $banners_rtl
|
||||
*/
|
||||
public function __construct(
|
||||
public string $id,
|
||||
public string $slug,
|
||||
public string $plugin,
|
||||
public string $url,
|
||||
public string $package,
|
||||
public string|null $requires,
|
||||
public string|null $tested,
|
||||
public string|null $requires_php,
|
||||
public Optional|array $requires_plugins,
|
||||
public Optional|array $compatibility,
|
||||
public Optional|array $icons,
|
||||
public Optional|array $banners,
|
||||
public Optional|array $banners_rtl,
|
||||
public Optional|string $new_version,
|
||||
public Optional|string $upgrade_notice,
|
||||
) {}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
#[Transforms(Plugin::class)]
|
||||
public static function fromPlugin(Plugin $plugin): array
|
||||
{
|
||||
$slug = $plugin->slug;
|
||||
return [
|
||||
'id' => "w.org/plugins/$slug",
|
||||
'slug' => $slug,
|
||||
'plugin' => "$slug/$slug.php", // gets rewritten to the "real" filename later. hacky, but it works for this.
|
||||
'new_version' => $plugin->version,
|
||||
'url' => "https://wordpress.org/plugins/$slug/",
|
||||
'package' => $plugin->download_link,
|
||||
'icons' => $plugin->icons,
|
||||
'banners' => $plugin->banners,
|
||||
'banners_rtl' => [],
|
||||
'requires' => $plugin->requires,
|
||||
'tested' => $plugin->tested,
|
||||
'requires_php' => $plugin->requires_php,
|
||||
'requires_plugins' => $plugin->requires_plugins,
|
||||
'compatibility' => $plugin->compatibility,
|
||||
// TODO: upgrade_notice (maybe in metadata somewhere?)
|
||||
];
|
||||
}
|
||||
}
|
48
app/Values/WpOrg/Plugins/QueryPluginsRequest.php
Normal file
48
app/Values/WpOrg/Plugins/QueryPluginsRequest.php
Normal file
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Values\WpOrg\Plugins;
|
||||
|
||||
use Bag\Attributes\StripExtraParameters;
|
||||
use Bag\Attributes\Transforms;
|
||||
use Bag\Bag;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
// Completely isomorphic to QueryThemesRequest, except $theme is replaced with $plugin. Hmm.
|
||||
// I'd look into refactoring it, but it's not like .org is going to add a new resource type anytime soon.
|
||||
// We can clean things up in the 2.0 API.
|
||||
#[StripExtraParameters]
|
||||
readonly class QueryPluginsRequest extends Bag
|
||||
{
|
||||
/** @param list<string>|null $tags */
|
||||
public function __construct(
|
||||
public ?string $search = null, // text to search
|
||||
public ?array $tags = null, // tag or set of tags
|
||||
public ?string $plugin = null, // slug of a specific plugin
|
||||
public ?string $author = null, // wp.org username of author
|
||||
public ?string $browse = null, // one of popular|top-rated|updated|new
|
||||
public mixed $fields = null,
|
||||
public int $page = 1,
|
||||
public int $per_page = 24,
|
||||
) {}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
#[Transforms(Request::class)]
|
||||
public static function fromRequest(Request $request): array
|
||||
{
|
||||
$query = $request->query();
|
||||
|
||||
$query['tags'] = (array)Arr::pull($query, 'tag', []);
|
||||
|
||||
// $defaultFields = [
|
||||
// 'description' => true,
|
||||
// 'rating' => true,
|
||||
// 'homepage' => true,
|
||||
// 'template' => true,
|
||||
// ];
|
||||
// $query['fields'] = self::getFields($request, $defaultFields);
|
||||
return $query;
|
||||
}
|
||||
}
|
18
app/Values/WpOrg/Plugins/QueryPluginsResponse.php
Normal file
18
app/Values/WpOrg/Plugins/QueryPluginsResponse.php
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Values\WpOrg\Plugins;
|
||||
|
||||
use App\Values\WpOrg\PageInfo;
|
||||
use Bag\Bag;
|
||||
use Bag\Collection;
|
||||
|
||||
readonly class QueryPluginsResponse extends Bag
|
||||
{
|
||||
public function __construct(
|
||||
public Collection $plugins,
|
||||
public PageInfo $info,
|
||||
) {}
|
||||
|
||||
}
|
|
@ -3,8 +3,10 @@
|
|||
namespace App\Values\WpOrg\Themes;
|
||||
|
||||
use Bag\Attributes\StripExtraParameters;
|
||||
use Bag\Attributes\Transforms;
|
||||
use Bag\Bag;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
#[StripExtraParameters]
|
||||
readonly class QueryThemesRequest extends Bag
|
||||
|
@ -28,17 +30,13 @@ readonly class QueryThemesRequest extends Bag
|
|||
public int $per_page = 24,
|
||||
) {}
|
||||
|
||||
public static function fromRequest(Request $request): self
|
||||
/** @return array<string, mixed> */
|
||||
#[Transforms(Request::class)]
|
||||
public static function fromRequest(Request $request): array
|
||||
{
|
||||
$req = $request->query();
|
||||
$query = $request->query();
|
||||
|
||||
// 'tag' is the query parameter, but we store it on the 'tags' field
|
||||
// we could probably do this with a mapping and cast instead, but this works too,
|
||||
// and we have to do custom processing on fields anyway.
|
||||
if (is_array($req) && array_key_exists('tag', $req)) {
|
||||
$req['tags'] = is_array($req['tag']) ? $req['tag'] : [$req['tag']];
|
||||
unset($req['tag']);
|
||||
}
|
||||
$query['tags'] = (array)Arr::pull($query, 'tag', []);
|
||||
|
||||
$defaultFields = [
|
||||
'description' => true,
|
||||
|
@ -47,7 +45,7 @@ readonly class QueryThemesRequest extends Bag
|
|||
'template' => true,
|
||||
];
|
||||
|
||||
$req['fields'] = self::getFields($request, $defaultFields);
|
||||
return static::from($req);
|
||||
$query['fields'] = self::getFields($request, $defaultFields);
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
|
|
18
app/Values/WpOrg/Themes/QueryThemesResponse.php
Normal file
18
app/Values/WpOrg/Themes/QueryThemesResponse.php
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Values\WpOrg\Themes;
|
||||
|
||||
use App\Values\WpOrg\PageInfo;
|
||||
use Bag\Bag;
|
||||
use Bag\Collection;
|
||||
|
||||
readonly class QueryThemesResponse extends Bag
|
||||
{
|
||||
public function __construct(
|
||||
public Collection $themes,
|
||||
public PageInfo $info,
|
||||
) {}
|
||||
|
||||
}
|
|
@ -9,8 +9,8 @@ trait ThemeFields
|
|||
public const allFields = [
|
||||
'description' => false,
|
||||
'downloaded' => false,
|
||||
'downloadlink' => false,
|
||||
'last_updated' => false,
|
||||
'download_link' => false,
|
||||
'last_updated_time' => false,
|
||||
'creation_time' => false,
|
||||
'parent' => false,
|
||||
'rating' => false,
|
||||
|
|
|
@ -2,8 +2,9 @@
|
|||
|
||||
namespace App\Values\WpOrg\Themes;
|
||||
|
||||
use App\Models\WpOrg\ThemeTag;
|
||||
use Bag\Attributes\Transforms;
|
||||
use Bag\Bag;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
readonly class ThemeHotTagsResponse extends Bag
|
||||
{
|
||||
|
@ -13,21 +14,10 @@ readonly class ThemeHotTagsResponse extends Bag
|
|||
public int $count,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Static method to create an instance from a Theme model.
|
||||
*
|
||||
* @param Collection<int,covariant array{
|
||||
* slug: string,
|
||||
* name: string,
|
||||
* count: int,
|
||||
* }> $themeTags
|
||||
* @return Collection<string, covariant ThemeHotTagsResponse>
|
||||
*/
|
||||
public static function fromCollection(Collection $themeTags): Collection
|
||||
/** @return array{slug: string, name: string, count: int} */
|
||||
#[Transforms(ThemeTag::class)]
|
||||
public static function fromThemeTag(ThemeTag $tag): array
|
||||
{
|
||||
return $themeTags->mapWithKeys(fn($theme)
|
||||
=> [
|
||||
$theme['slug'] => self::from($theme),
|
||||
]);
|
||||
return ['slug' => $tag->slug, 'name' => $tag->name, 'count' => $tag->themes_count];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,8 +30,9 @@ readonly class ThemeInformationRequest extends Bag
|
|||
'sections' => true,
|
||||
'rating' => true,
|
||||
'downloaded' => true,
|
||||
'downloadlink' => true,
|
||||
'download_link' => true,
|
||||
'last_updated' => true,
|
||||
'last_updated_time' => true,
|
||||
'homepage' => true,
|
||||
'tags' => true,
|
||||
'template' => true,
|
||||
|
@ -43,6 +44,7 @@ readonly class ThemeInformationRequest extends Bag
|
|||
}
|
||||
|
||||
$req['fields'] = static::getFields($request, $defaultFields);
|
||||
$req['last_updated_time'] = $req['last_updated'] ?? true;
|
||||
|
||||
return static::from($req);
|
||||
}
|
||||
|
|
141
app/Values/WpOrg/Themes/ThemeResponse.php
Normal file
141
app/Values/WpOrg/Themes/ThemeResponse.php
Normal file
|
@ -0,0 +1,141 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Values\WpOrg\Themes;
|
||||
|
||||
use App\Models\WpOrg\Theme;
|
||||
use App\Values\WpOrg\Author;
|
||||
use Bag\Attributes\Hidden;
|
||||
use Bag\Attributes\Transforms;
|
||||
use Bag\Bag;
|
||||
use Bag\Values\Optional;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
readonly class ThemeResponse extends Bag
|
||||
{
|
||||
/**
|
||||
* @param Optional|array{'1': int, '2': int, '3': int, '4': int, '5': int, } $ratings
|
||||
* @param Optional|array<string, mixed> $sections
|
||||
* @param Optional|array<string, mixed> $tags
|
||||
* @param Optional|array<string, mixed> $versions
|
||||
* @param Optional|array<string, mixed> $requires
|
||||
* @param Optional|array<string, mixed> $screenshots
|
||||
* @param Optional|array<string, mixed> $photon_screenshots
|
||||
* @param Optional|array<string, mixed> $trac_tickets
|
||||
*/
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public string $slug,
|
||||
public string $version,
|
||||
public string $preview_url,
|
||||
public Optional|Author|string $author,
|
||||
public Optional|string $description,
|
||||
public Optional|string $screenshot_url,
|
||||
public Optional|array $ratings, // TODO: ensure this casts to array correctly
|
||||
public Optional|int $rating,
|
||||
public Optional|int $num_ratings,
|
||||
public Optional|string $reviews_url,
|
||||
public Optional|int $downloaded,
|
||||
public Optional|int $active_installs,
|
||||
public Optional|string $last_updated,
|
||||
public Optional|string $last_updated_time,
|
||||
public Optional|string $creation_time,
|
||||
public Optional|string $homepage,
|
||||
public Optional|array $sections,
|
||||
public Optional|string $download_link,
|
||||
public Optional|array $tags,
|
||||
public Optional|array $versions,
|
||||
public Optional|array $requires,
|
||||
public Optional|string $requires_php,
|
||||
public Optional|bool $is_commercial,
|
||||
public Optional|string $external_support_url,
|
||||
public Optional|bool $is_community,
|
||||
public Optional|string $external_repository_url,
|
||||
#[Hidden]
|
||||
public Optional|Author $extended_author,
|
||||
|
||||
// Always empty for now
|
||||
public Optional|string $parent,
|
||||
public Optional|int $screenshot_count,
|
||||
public Optional|array $screenshots,
|
||||
public Optional|string $theme_url,
|
||||
public Optional|array $photon_screenshots,
|
||||
public Optional|array $trac_tickets,
|
||||
public Optional|string $upload_date,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
* @noinspection ProperNullCoalescingOperatorUsageInspection (it's fine here)
|
||||
*/
|
||||
#[Transforms(Theme::class)]
|
||||
public static function fromTheme(Theme $theme): array
|
||||
{
|
||||
// Note we fill in all fields, then strip out any not-requested Optional. such silliness is compatibility.
|
||||
|
||||
$none = new Optional();
|
||||
return [
|
||||
'name' => $theme->name,
|
||||
'slug' => $theme->slug,
|
||||
'version' => $theme->version,
|
||||
'preview_url' => $theme->preview_url,
|
||||
'author' => $theme->author ?? $none,
|
||||
// gets converted to $theme->author->user_nicename unless extended_author=true
|
||||
'description' => $theme->description ?? $none,
|
||||
'screenshot_url' => $theme->screenshot_url ?? $none,
|
||||
'ratings' => $theme->ratings ?? $none,
|
||||
'rating' => $theme->rating ?? $none,
|
||||
'num_ratings' => $theme->num_ratings ?? $none,
|
||||
'reviews_url' => $theme->reviews_url ?? $none,
|
||||
'downloaded' => $theme->downloaded ?? $none,
|
||||
'active_installs' => $theme->active_installs ?? $none,
|
||||
'last_updated' => $theme->last_updated->format('Y-m-d') ?? $none,
|
||||
'last_updated_time' => $theme->last_updated->format('Y-m-d H:i:s') ?? $none,
|
||||
'creation_time' => $theme->creation_time->format('Y-m-d H:i:s') ?? $none,
|
||||
'homepage' => "https://wordpress.org/themes/{$theme->slug}/",
|
||||
'sections' => $theme->sections ?? $none,
|
||||
'download_link' => $theme->download_link ?? $none,
|
||||
'tags' => $theme->tagsArray(),
|
||||
'versions' => $theme->versions ?? $none,
|
||||
'requires' => $theme->requires ?? $none,
|
||||
'requires_php' => $theme->requires_php ?? $none,
|
||||
'is_commercial' => $theme->is_commercial ?? $none,
|
||||
'external_support_url' => $theme->external_support_url,
|
||||
'is_community' => $theme->is_community ?? $none,
|
||||
'external_repository_url' => $theme->external_repository_url,
|
||||
|
||||
// hidden
|
||||
'extended_author' => $theme->author,
|
||||
|
||||
// eventual support
|
||||
'parent' => $none,
|
||||
'screenshot_count' => $none,
|
||||
'screenshots' => $none,
|
||||
'theme_url' => $none,
|
||||
'photon_screenshots' => $none,
|
||||
'trac_tickets' => $none,
|
||||
'upload_date' => $none,
|
||||
];
|
||||
}
|
||||
|
||||
/** @param array<string, bool> $fields */
|
||||
public function withFields(array $fields): static
|
||||
{
|
||||
$none = new Optional();
|
||||
$extendedAuthor = Arr::pull($fields, 'extended_author', false);
|
||||
|
||||
$omit = collect($fields)
|
||||
->filter(fn($val, $key) => !$val)
|
||||
->mapWithKeys(fn($val, $key) => [$key => $none])
|
||||
->toArray();
|
||||
|
||||
$self = $this->with($omit);
|
||||
|
||||
if (!$extendedAuthor) {
|
||||
$self = $self->with(['author' => $self->author->user_nicename]);
|
||||
}
|
||||
|
||||
return $self;
|
||||
}
|
||||
}
|
|
@ -2,53 +2,46 @@
|
|||
|
||||
namespace App\Values\WpOrg\Themes;
|
||||
|
||||
use Bag\Attributes\Transforms;
|
||||
use Bag\Bag;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use function Safe\json_decode;
|
||||
|
||||
readonly class ThemeUpdateCheckRequest extends Bag
|
||||
{
|
||||
/**
|
||||
/**
|
||||
* @phpstan-type TranslationMetadata array{
|
||||
* POT-Creation-Date: string, // Creation date of the POT file
|
||||
* PO-Revision-Date: string, // Revision date of the PO file
|
||||
* Project-Id-Version: string, // Project version info
|
||||
* X-Generator: string // Generator software info
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param string $active // Active theme slug
|
||||
* @param array<string,array{
|
||||
* "Version": string,
|
||||
* }> $themes // Array of theme slugs and their current versions
|
||||
* @param array<string,array<string,array{
|
||||
* POT-Creation-Date: string,
|
||||
* PO-Revision-Date: string,
|
||||
* Project-Id-Version: string,
|
||||
* X-Generator: string
|
||||
* }>> $translations
|
||||
* @param string[] $locale // Array of locale strings
|
||||
* }
|
||||
*/
|
||||
readonly class ThemeUpdateCheckRequest extends Bag
|
||||
{
|
||||
/**
|
||||
* @param string $active slug of currently active theme
|
||||
* @param array<string, array{"Version": string}> $themes
|
||||
* @param array<string, array<string, TranslationMetadata>> $translations
|
||||
* @param list<string> $locale
|
||||
*/
|
||||
public function __construct(
|
||||
public ?string $active = null, // text to search
|
||||
public ?array $themes = null,
|
||||
public ?array $translations = null,
|
||||
public ?array $locale = null,
|
||||
public string $active,
|
||||
public array $themes,
|
||||
public array $translations,
|
||||
public array $locale,
|
||||
) {}
|
||||
|
||||
public static function fromRequest(Request $request): self
|
||||
/** @return array<string, mixed> */
|
||||
#[Transforms(Request::class)]
|
||||
public static function fromRequest(Request $request): array
|
||||
{
|
||||
$themes = $request->post('themes');
|
||||
$locale = $request->post('locale');
|
||||
$translations = $request->post('translations');
|
||||
$themeData = json_decode($themes, true);
|
||||
return static::from([
|
||||
'active' => $themeData['active'],
|
||||
'themes' => $themeData['themes'],
|
||||
'locale' => json_decode($locale, true),
|
||||
'translations' => json_decode($translations, true),
|
||||
]);
|
||||
$decode = fn($key) => json_decode($request->post($key), true);
|
||||
$themes = $decode('themes');
|
||||
return [
|
||||
'active' => $themes['active'],
|
||||
'themes' => $themes['themes'],
|
||||
'locale' => $decode('locale'),
|
||||
'translations' => $decode('translations'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,25 +9,26 @@ use Illuminate\Support\Collection;
|
|||
readonly class ThemeUpdateCheckResponse extends Bag
|
||||
{
|
||||
/**
|
||||
* @param Collection<string,ThemeUpdateData> $themes
|
||||
* @param Collection<string,ThemeUpdateData> $no_update
|
||||
* @param Collection<string, ThemeUpdateData> $themes
|
||||
* @param Collection<string, ThemeUpdateData> $no_update
|
||||
* @param Collection<array-key, mixed> $translations
|
||||
*/
|
||||
public function __construct(
|
||||
public Collection $themes,
|
||||
public Collection $no_update,
|
||||
public mixed $translations,
|
||||
public Collection $translations,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param Collection<int,Theme> $themes
|
||||
* @param Collection<int,Theme> $noUpdate
|
||||
* @param iterable<array-key, Theme> $themes
|
||||
* @param iterable<array-key, Theme> $no_update
|
||||
*/
|
||||
public static function fromData(Collection $themes, Collection $noUpdate): self
|
||||
public static function fromResults(iterable $themes, iterable $no_update): self
|
||||
{
|
||||
return new self(
|
||||
themes: ThemeUpdateData::fromModelCollection($themes),
|
||||
no_update: ThemeUpdateData::fromModelCollection($noUpdate),
|
||||
translations: [],
|
||||
themes: ThemeUpdateData::collect($themes)->keyBy('theme'),
|
||||
no_update: ThemeUpdateData::collect($no_update)->keyBy('theme'),
|
||||
translations: collect(), // TODO
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Values\WpOrg\Themes;
|
||||
|
||||
use Bag\Bag;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use function Safe\json_decode;
|
||||
|
||||
readonly class ThemeUpdateCheckTranslationCollection extends Bag
|
||||
{
|
||||
/**
|
||||
* @param ?array<string,mixed> $themes
|
||||
* @param ?array<string,mixed> $translations
|
||||
* @param ?string[] $locale
|
||||
*/
|
||||
public function __construct(
|
||||
public ?string $active = null, // text to search
|
||||
public ?array $themes = null,
|
||||
public ?array $translations = null,
|
||||
public ?array $locale = null,
|
||||
) {}
|
||||
|
||||
public static function fromRequest(Request $request): self
|
||||
{
|
||||
$themes = $request->post('themes');
|
||||
$locale = $request->post('locale');
|
||||
$translations = $request->post('translations');
|
||||
$themeData = json_decode($themes, true);
|
||||
return static::from([
|
||||
'active' => $themeData['active'],
|
||||
'themes' => $themeData['themes'],
|
||||
'locale' => json_decode($locale, true),
|
||||
'translations' => json_decode($translations, true),
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -3,8 +3,8 @@
|
|||
namespace App\Values\WpOrg\Themes;
|
||||
|
||||
use App\Models\WpOrg\Theme;
|
||||
use Bag\Attributes\Transforms;
|
||||
use Bag\Bag;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
readonly class ThemeUpdateData extends Bag
|
||||
{
|
||||
|
@ -18,25 +18,18 @@ readonly class ThemeUpdateData extends Bag
|
|||
public ?string $requires_php,
|
||||
) {}
|
||||
|
||||
public static function fromModel(Theme $theme): self
|
||||
/** @return array<string, mixed> */
|
||||
#[Transforms(Theme::class)]
|
||||
public static function fromTheme(Theme $theme): array
|
||||
{
|
||||
return new self(
|
||||
name: $theme->name,
|
||||
theme: $theme->slug,
|
||||
new_version: $theme->version,
|
||||
url: $theme->download_link,
|
||||
package: $theme->download_link,
|
||||
requires: $theme->requires,
|
||||
requires_php: $theme->requires_php,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int,Theme> $themes
|
||||
* @return Collection<string,ThemeUpdateData>
|
||||
*/
|
||||
public static function fromModelCollection(Collection $themes): Collection
|
||||
{
|
||||
return $themes->mapWithKeys(fn($theme) => [$theme->slug => self::fromModel($theme)]);
|
||||
return [
|
||||
'name' => $theme->name,
|
||||
'theme' => $theme->slug,
|
||||
'new_version' => $theme->version,
|
||||
'url' => $theme->download_link,
|
||||
'package' => $theme->download_link,
|
||||
'requires' => $theme->requires,
|
||||
'requires_php' => $theme->requires_php,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,11 +19,10 @@ function query_plugin_uri(array $params = []): string
|
|||
return "/plugins/info/1.2?" . http_build_query(['action' => 'query_plugibs', ...$params]);
|
||||
}
|
||||
|
||||
it('returns 400 when slug is missing', function () {
|
||||
it('returns 422 when slug is missing', function () {
|
||||
$this
|
||||
->getJson('/plugins/info/1.2?action=plugin_information')
|
||||
->assertStatus(400)
|
||||
->assertJson(['error' => 'Slug is required']);
|
||||
->assertStatus(422);
|
||||
});
|
||||
|
||||
it('returns 404 when plugin does not exist', function () {
|
||||
|
|
|
@ -28,8 +28,8 @@ it('returns plugin updates from minimal input', function () {
|
|||
->post('/plugins/update-check/1.1', [
|
||||
'plugins' => json_encode([
|
||||
'plugins' => [
|
||||
'frobnicator' => ['Version' => '1.0.2'], // out of date
|
||||
'transmogrifier' => ['Version' => '0.5'], // up to date
|
||||
'frobnicator/frobber.php' => ['Version' => '1.0.2'], // out of date
|
||||
'transmogrifier.php' => ['Version' => '0.5'], // up to date
|
||||
],
|
||||
]),
|
||||
'translations' => json_encode([]),
|
||||
|
@ -38,8 +38,8 @@ it('returns plugin updates from minimal input', function () {
|
|||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'plugins' => [
|
||||
'frobnicator' => [
|
||||
'plugin' => 'frobnicator',
|
||||
'frobnicator/frobber.php' => [
|
||||
'plugin' => 'frobnicator/frobber.php',
|
||||
'slug' => 'frobnicator',
|
||||
'new_version' => '1.2.3',
|
||||
],
|
||||
|
@ -82,8 +82,8 @@ it('includes no_update when all=true', function () {
|
|||
->post('/plugins/update-check/1.1?all=true', [
|
||||
'plugins' => json_encode([
|
||||
'plugins' => [
|
||||
'frobnicator' => ['Version' => '1.0.2'], // out of date
|
||||
'transmogrifier' => ['Version' => '0.5'], // up to date
|
||||
'frobnicator/frobber.php' => ['Version' => '1.0.2'], // out of date
|
||||
'transmogrifier.php' => ['Version' => '0.5'], // up to date
|
||||
],
|
||||
]),
|
||||
'translations' => json_encode([]),
|
||||
|
@ -92,8 +92,8 @@ it('includes no_update when all=true', function () {
|
|||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'plugins' => [
|
||||
'frobnicator' => [
|
||||
'plugin' => 'frobnicator',
|
||||
'frobnicator/frobber.php' => [
|
||||
'plugin' => 'frobnicator/frobber.php',
|
||||
'slug' => 'frobnicator',
|
||||
'new_version' => '1.2.3',
|
||||
],
|
||||
|
@ -102,7 +102,7 @@ it('includes no_update when all=true', function () {
|
|||
->assertJsonMissingPath('plugins.transmogrifier')
|
||||
->assertExactJsonStructure([
|
||||
'plugins' => [
|
||||
'frobnicator' => [
|
||||
'frobnicator/frobber.php' => [
|
||||
'banners' => ['high', 'low'],
|
||||
'banners_rtl',
|
||||
'compatibility' => [
|
||||
|
@ -123,7 +123,7 @@ it('includes no_update when all=true', function () {
|
|||
],
|
||||
],
|
||||
'no_update' => [
|
||||
'transmogrifier' => [
|
||||
'transmogrifier.php' => [
|
||||
'banners' => ['high', 'low'],
|
||||
'banners_rtl',
|
||||
'compatibility' => [
|
||||
|
@ -146,76 +146,3 @@ it('includes no_update when all=true', function () {
|
|||
'translations',
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
// '{\n
|
||||
// "no_update": {\n
|
||||
// "transmogrifier": {\n
|
||||
// "banners": {\n
|
||||
// "high": "https://via.placeholder.com/1544x500.png/0022cc?text=molestias",\n
|
||||
// "low": "https://via.placeholder.com/772x250.png/001199?text=minima"\n
|
||||
// },\n
|
||||
// "banners_rtl": [],\n
|
||||
// "compatibility": {\n
|
||||
// "php": {\n
|
||||
// "minimum": "7.4",\n
|
||||
// "recommended": "8.2"\n
|
||||
// },\n
|
||||
// "wordpress": {\n
|
||||
// "maximum": "8.2.99",\n
|
||||
// "minimum": "2.16.32",\n
|
||||
// "tested": "8.44.14"\n
|
||||
// }\n
|
||||
// },\n
|
||||
// "icons": {\n
|
||||
// "1x": "https://via.placeholder.com/128x128.png/006699?text=laboriosam",\n
|
||||
// "2x": "https://via.placeholder.com/256x256.png/00ee11?text=ut"\n
|
||||
// },\n
|
||||
// "id": "w.org/plugins/transmogrifier",\n
|
||||
// "new_version": "0.5",\n
|
||||
// "package": "http://www.vonrueden.com/iusto-voluptatem-delectus-tempora-placeat-tempora-exercitationem-ducimus-blanditiis.html",\n
|
||||
// "plugin": "transmogrifier",\n
|
||||
// "requires": "3.22.80",\n
|
||||
// "requires_php": "8.2",\n
|
||||
// "requires_plugins": [],\n
|
||||
// "slug": "transmogrifier",\n
|
||||
// "tested": "WordPress 4.3.39",\n
|
||||
// "url": "https://wordpress.org/plugins/transmogrifier/"\n
|
||||
// }\n
|
||||
// },\n
|
||||
// "plugins": {\n
|
||||
// "frobnicator": {\n
|
||||
// "banners": {\n
|
||||
// "high": "https://via.placeholder.com/1544x500.png/00ee22?text=laudantium",\n
|
||||
// "low": "https://via.placeholder.com/772x250.png/0077cc?text=soluta"\n
|
||||
// },\n
|
||||
// "banners_rtl": [],\n
|
||||
// "compatibility": {\n
|
||||
// "php": {\n
|
||||
// "minimum": "8.1",\n
|
||||
// "recommended": "8.0"\n
|
||||
// },\n
|
||||
// "wordpress": {\n
|
||||
// "maximum": "9.46.6",\n
|
||||
// "minimum": "1.4.31",\n
|
||||
// "tested": "9.91.94"\n
|
||||
// }\n
|
||||
// },\n
|
||||
// "icons": {\n
|
||||
// "1x": "https://via.placeholder.com/128x128.png/00ffff?text=minima",\n
|
||||
// "2x": "https://via.placeholder.com/256x256.png/0077bb?text=quia"\n
|
||||
// },\n
|
||||
// "id": "w.org/plugins/frobnicator",\n
|
||||
// "new_version": "1.2.3",\n
|
||||
// "package": "http://shields.com/",\n
|
||||
// "plugin": "frobnicator",\n
|
||||
// "requires": "2.64.58",\n
|
||||
// "requires_php": "7.3",\n
|
||||
// "requires_plugins": [],\n
|
||||
// "slug": "frobnicator",\n
|
||||
// "tested": "WordPress 6.4.16",\n
|
||||
// "url": "https://wordpress.org/plugins/frobnicator/"\n
|
||||
// }\n
|
||||
// },\n
|
||||
// "translations": []\n
|
||||
// }'
|
||||
|
|
|
@ -111,7 +111,7 @@ it('returns theme_information (v1.2)', function () {
|
|||
'download_link' => 'https://api.aspiredev.org/download/my-theme',
|
||||
'downloaded' => 1000,
|
||||
'external_repository_url' => 'https://test.com',
|
||||
'external_support_url' => false,
|
||||
'external_support_url' => "",
|
||||
'homepage' => 'https://wordpress.org/themes/my-theme/',
|
||||
'is_commercial' => false,
|
||||
'is_community' => true,
|
||||
|
@ -121,7 +121,6 @@ it('returns theme_information (v1.2)', function () {
|
|||
'num_ratings' => 6,
|
||||
'preview_url' => 'https://wp-themes.com/my-theme',
|
||||
'rating' => 5,
|
||||
'requires' => null,
|
||||
'requires_php' => '5.6',
|
||||
'reviews_url' => 'https://wp-themes.com/my-theme/reviews',
|
||||
'screenshot_url' => 'https://wp-themes.com/my-theme/screenshot.png',
|
||||
|
@ -141,7 +140,7 @@ it('returns theme query results (v1.1)', function () {
|
|||
$this
|
||||
->get('/themes/info/1.1?action=query_themes')
|
||||
->assertStatus(200)
|
||||
->assertExactJson([
|
||||
->assertJson([
|
||||
'info' => ['page' => 1, 'pages' => 1, 'results' => 1],
|
||||
'themes' => [
|
||||
[
|
||||
|
@ -164,7 +163,7 @@ it('returns theme query results (v1.2)', function () {
|
|||
$this
|
||||
->get('/themes/info/1.2?action=query_themes')
|
||||
->assertStatus(200)
|
||||
->assertExactJson([
|
||||
->assertJson([
|
||||
'info' => ['page' => 1, 'pages' => 1, 'results' => 1],
|
||||
'themes' => [
|
||||
[
|
||||
|
@ -186,7 +185,6 @@ it('returns theme query results (v1.2)', function () {
|
|||
'num_ratings' => 6,
|
||||
'preview_url' => 'https://wp-themes.com/my-theme',
|
||||
'rating' => 5,
|
||||
'requires' => null,
|
||||
'requires_php' => '5.6',
|
||||
'screenshot_url' => 'https://wp-themes.com/my-theme/screenshot.png',
|
||||
'slug' => 'my-theme',
|
||||
|
@ -197,6 +195,49 @@ it('returns theme query results (v1.2)', function () {
|
|||
|
||||
});
|
||||
|
||||
it('returns theme query results for tag (v1.2)', function () {
|
||||
$this
|
||||
->get('/themes/info/1.2?action=query_themes&tag=black')
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'info' => ['page' => 1, 'pages' => 1, 'results' => 1],
|
||||
'themes' => [
|
||||
[
|
||||
'author' => [
|
||||
'author' => 'Tmeister',
|
||||
'author_url' => 'https://wp-themes.com/author/tmeister',
|
||||
'avatar' => 'https://avatars.wp.org/tmeister',
|
||||
'display_name' => 'Tmeister',
|
||||
'profile' => 'https://profiles.wp.org/tmeister',
|
||||
'user_nicename' => 'tmeister',
|
||||
],
|
||||
'description' => 'My Theme',
|
||||
'external_repository_url' => 'https://test.com',
|
||||
'external_support_url' => '',
|
||||
'homepage' => 'https://wordpress.org/themes/my-theme/',
|
||||
'is_commercial' => false,
|
||||
'is_community' => true,
|
||||
'name' => 'My Theme',
|
||||
'num_ratings' => 6,
|
||||
'preview_url' => 'https://wp-themes.com/my-theme',
|
||||
'rating' => 5,
|
||||
'requires_php' => '5.6',
|
||||
'screenshot_url' => 'https://wp-themes.com/my-theme/screenshot.png',
|
||||
'slug' => 'my-theme',
|
||||
'version' => '1.2.1',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this
|
||||
->get('/themes/info/1.2?action=query_themes&tag=orange')
|
||||
->assertStatus(200)
|
||||
->assertExactJson([
|
||||
'info' => ['page' => 1, 'pages' => 0, 'results' => 0], // page 1 of 0 is a bit odd but it is correct
|
||||
'themes' => [],
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns hot tags results (v1.1)', function () {
|
||||
$this
|
||||
->get('/themes/info/1.1?action=hot_tags')
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue