Skip to content
Draft
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
3 changes: 2 additions & 1 deletion app/src/Bakery/BakeCommandListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ public function __invoke(BakeCommandEvent $event): void
$event->setCommands([
'debug',
'assets:build',
'clear-cache'
'clear-cache',
'search:index'
]);
}
}
90 changes: 90 additions & 0 deletions app/src/Bakery/SearchIndexCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

declare(strict_types=1);

/*
* UserFrosting Learn (http://www.userfrosting.com)
*
* @link https://github.com/userfrosting/Learn
* @copyright Copyright (c) 2025 Alexander Weissman & Louis Charette
* @license https://github.com/userfrosting/Learn/blob/main/LICENSE.md (MIT License)
*/

namespace UserFrosting\Learn\Bakery;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use UserFrosting\Learn\Search\SearchIndex;
use UserFrosting\Sprinkle\Core\Bakery\BaseCommand;

/**
* Bakery command to rebuild the search index for documentation.
*/
class SearchIndexCommand extends BaseCommand

Check failure on line 25 in app/src/Bakery/SearchIndexCommand.php

View workflow job for this annotation

GitHub Actions / PHPStan - PHP 8.4

Class UserFrosting\Learn\Bakery\SearchIndexCommand extends unknown class UserFrosting\Sprinkle\Core\Bakery\BaseCommand.
{
/**
* @param SearchIndex $searchIndex
*/
public function __construct(
protected SearchIndex $searchIndex,
) {
parent::__construct();

Check failure on line 33 in app/src/Bakery/SearchIndexCommand.php

View workflow job for this annotation

GitHub Actions / PHPStan - PHP 8.4

UserFrosting\Learn\Bakery\SearchIndexCommand::__construct() calls parent::__construct() but UserFrosting\Learn\Bakery\SearchIndexCommand does not extend any class.
}

/**
* {@inheritdoc}
*/
protected function configure(): void
{
$this->setName('search:index')

Check failure on line 41 in app/src/Bakery/SearchIndexCommand.php

View workflow job for this annotation

GitHub Actions / PHPStan - PHP 8.4

Call to an undefined method UserFrosting\Learn\Bakery\SearchIndexCommand::setName().
->setDescription('Build or rebuild the search index for documentation')
->addOption(
'version',
null,
InputOption::VALUE_OPTIONAL,
'Documentation version to index (omit to index all versions)'
)
->addOption(
'clear',
null,
InputOption::VALUE_NONE,
'Clear the search index before rebuilding'
);
}

/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->io->title('Documentation Search Index');

Check failure on line 62 in app/src/Bakery/SearchIndexCommand.php

View workflow job for this annotation

GitHub Actions / PHPStan - PHP 8.4

Access to an undefined property UserFrosting\Learn\Bakery\SearchIndexCommand::$io.

/** @var string|null $version */
$version = $input->getOption('version');
$clear = $input->getOption('clear');

// Clear index if requested
if ($clear) {

Check failure on line 69 in app/src/Bakery/SearchIndexCommand.php

View workflow job for this annotation

GitHub Actions / PHPStan - PHP 8.4

Only booleans are allowed in an if condition, mixed given.
$this->io->writeln('Clearing search index...');

Check failure on line 70 in app/src/Bakery/SearchIndexCommand.php

View workflow job for this annotation

GitHub Actions / PHPStan - PHP 8.4

Access to an undefined property UserFrosting\Learn\Bakery\SearchIndexCommand::$io.
$this->searchIndex->clearIndex($version);
$this->io->success('Search index cleared.');

Check failure on line 72 in app/src/Bakery/SearchIndexCommand.php

View workflow job for this annotation

GitHub Actions / PHPStan - PHP 8.4

Access to an undefined property UserFrosting\Learn\Bakery\SearchIndexCommand::$io.
}

// Build index
$versionText = $version !== null ? "version {$version}" : 'all versions';
$this->io->writeln("Building search index for {$versionText}...");

Check failure on line 77 in app/src/Bakery/SearchIndexCommand.php

View workflow job for this annotation

GitHub Actions / PHPStan - PHP 8.4

Access to an undefined property UserFrosting\Learn\Bakery\SearchIndexCommand::$io.

try {
$count = $this->searchIndex->buildIndex($version);
$this->io->success("Search index built successfully. Indexed {$count} pages.");

Check failure on line 81 in app/src/Bakery/SearchIndexCommand.php

View workflow job for this annotation

GitHub Actions / PHPStan - PHP 8.4

Access to an undefined property UserFrosting\Learn\Bakery\SearchIndexCommand::$io.
} catch (\Exception $e) {
$this->io->error("Failed to build search index: {$e->getMessage()}");

Check failure on line 83 in app/src/Bakery/SearchIndexCommand.php

View workflow job for this annotation

GitHub Actions / PHPStan - PHP 8.4

Access to an undefined property UserFrosting\Learn\Bakery\SearchIndexCommand::$io.

return Command::FAILURE;
}

return Command::SUCCESS;
}
}
76 changes: 76 additions & 0 deletions app/src/Controller/SearchController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

/*
* UserFrosting Learn (http://www.userfrosting.com)
*
* @link https://github.com/userfrosting/Learn
* @copyright Copyright (c) 2025 Alexander Weissman & Louis Charette
* @license https://github.com/userfrosting/Learn/blob/main/LICENSE.md (MIT License)
*/

namespace UserFrosting\Learn\Controller;

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use UserFrosting\Learn\Search\SearchService;

/**
* Controller for the documentation search API.
*/
class SearchController
{
public function __construct(
protected SearchService $searchService,
) {
}

/**
* Search documentation pages.
* Request type: GET.
*
* Query parameters:
* - q: Search query (required)
* - version: Documentation version to search (optional, defaults to latest)
* - page: Page number for pagination (optional, default: 1)
* - size: Number of results per page (optional, default: 10, max: 100)
*
* @param Request $request
* @param Response $response
*/
public function search(Request $request, Response $response): Response
{
$params = $request->getQueryParams();

// Get query parameter
$query = $params['q'] ?? '';

if (empty($query)) {
$result = [
'rows' => [],
'count' => 0,
'count_filtered' => 0,
];

$response->getBody()->write(json_encode($result, JSON_THROW_ON_ERROR));

return $response->withHeader('Content-Type', 'application/json');
}

// Get pagination parameters
$page = isset($params['page']) ? max(1, (int) $params['page']) : 1;
$size = isset($params['size']) ? min(100, max(1, (int) $params['size'])) : 10;

// Get version parameter
$version = $params['version'] ?? null;

// Perform search
$result = $this->searchService->search($query, $version, $page, $size);

// Write JSON response
$response->getBody()->write(json_encode($result, JSON_THROW_ON_ERROR));

return $response->withHeader('Content-Type', 'application/json');
}
}
5 changes: 5 additions & 0 deletions app/src/MyRoutes.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,18 @@

use Slim\App;
use UserFrosting\Learn\Controller\DocumentationController;
use UserFrosting\Learn\Controller\SearchController;
use UserFrosting\Learn\Middleware\TwigGlobals;
use UserFrosting\Routes\RouteDefinitionInterface;

class MyRoutes implements RouteDefinitionInterface
{
public function register(App $app): void
{
// Route for search API
$app->get('/api/search', [SearchController::class, 'search'])
->setName('api.search');

// Route for versioned and non-versioned images
$app->get('/{version:\d+\.\d+}/images/{path:.*}', [DocumentationController::class, 'imageVersioned'])
->add(TwigGlobals::class)
Expand Down
19 changes: 18 additions & 1 deletion app/src/Recipe.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@
use UserFrosting\Learn\Bakery\BakeCommandListener;
use UserFrosting\Learn\Bakery\DebugCommandListener;
use UserFrosting\Learn\Bakery\DebugVerboseCommandListener;
use UserFrosting\Learn\Bakery\SearchIndexCommand;
use UserFrosting\Learn\Bakery\SetupCommandListener;
use UserFrosting\Learn\Listeners\ResourceLocatorInitiated;
use UserFrosting\Learn\ServicesProvider\MarkdownService;
use UserFrosting\Learn\ServicesProvider\SearchServicesProvider;
use UserFrosting\Learn\Twig\Extensions\FileTreeExtension;
use UserFrosting\Sprinkle\BakeryRecipe;
use UserFrosting\Sprinkle\Core\Bakery\Event\BakeCommandEvent;
use UserFrosting\Sprinkle\Core\Bakery\Event\DebugCommandEvent;
use UserFrosting\Sprinkle\Core\Bakery\Event\DebugVerboseCommandEvent;
Expand All @@ -35,7 +38,8 @@
class Recipe implements
SprinkleRecipe,
EventListenerRecipe,
TwigExtensionRecipe
TwigExtensionRecipe,
BakeryRecipe
{
/**
* Return the Sprinkle name.
Expand Down Expand Up @@ -104,6 +108,19 @@ public function getServices(): array
{
return [
MarkdownService::class,
SearchServicesProvider::class,
];
}

/**
* Return an array of all registered Bakery Commands.
*
* {@inheritdoc}
*/
public function getBakeryCommands(): array
{
return [
SearchIndexCommand::class,
];
}

Expand Down
Loading
Loading