wip: 2025-02-02T17:58:20+0100 (1738515500)

This commit is contained in:
Victor Westerlund 2025-02-02 20:56:40 +01:00
parent 0dd247408e
commit a12c9bc792
7 changed files with 206 additions and 26 deletions

View file

@ -0,0 +1,35 @@
<?php
use vlw\MySQL\Operators;
use Reflect\{Response, Path};
use ReflectRules\{Ruleset, Rules, Type};
use VLW\Database\Database;
use VLW\Database\Tables\Search\SearchTable;
require_once Path::root("src/Database/Database.php");
require_once Path::root("src/Database/Tables/Search/Search.php");
class DELETE_Search extends Database {
protected Ruleset $ruleset;
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->POST([
(new Rules(SearchTable::ID->value))
->required()
->type(Type::STRING)
->min(2)
->max(parent::SIZE_VARCHAR)
]);
$this->ruleset->validate_or_exit();
parent::__construct();
}
public function main(): Response {
return $this->db->for(SearchTable::NAME)->delete($_POST) === true ? new Response() : new Response("", 500);
}
}

View file

@ -1,12 +1,16 @@
<?php
use vlw\MySQL\Operators;
use Reflect\{Response, Path};
use Reflect\{Response, Path, Call};
use ReflectRules\{Ruleset, Rules, Type};
use VLW\API\Endpoints;
use VLW\Database\Database;
use VLW\Database\Tables\Search\SearchTable;
use const VLW\SEARCH_UPDATE_CACHE_PARM;
use VLW\Database\Tables\Search\{SearchTable, SearchCategoryEnum};
require_once Path::root("src/Consts.php");
require_once Path::root("src/API/Endpoints.php");
require_once Path::root("src/Database/Database.php");
require_once Path::root("src/Database/Tables/Search/Search.php");
@ -18,10 +22,24 @@
$this->ruleset->GET([
(new Rules(SearchTable::QUERY->value))
->required()
->type(Type::STRING)
->min(2)
->max(parent::SIZE_VARCHAR)
->default(null),
(new Rules(SearchTable::ID->value))
->type(Type::STRING)
->min(10)
->max(10)
->default(null),
(new Rules(SearchTable::CATEGORY->value))
->type(Type::ENUM, SearchCategoryEnum::names())
->default(null),
(new Rules(SEARCH_UPDATE_CACHE_PARM))
->type(Type::BOOLEAN)
->default(false)
]);
$this->ruleset->validate_or_exit();
@ -29,20 +47,47 @@
parent::__construct();
}
private static function get_query(): string {
preg_match_all("/[a-zA-Z0-9]+/", $_GET[SearchTable::QUERY->value], $matches);
return strtolower(implode("", $matches[0]));
}
public function main(): Response {
$result = $this->db
->for(SearchTable::NAME)
->where([
// Freshen cache if update flag is set
if ($_GET[SEARCH_UPDATE_CACHE_PARM]) {
(new Call(Endpoints::SEARCH->value))->post();
}
$result = $this->db->for(SearchTable::NAME);
if ($_GET[SearchTable::ID->value]) {
$result = $result->where([SearchTable::ID->value => $_GET[SearchTable::ID->value]]);
} else if ($_GET[SearchTable::QUERY->value]) {
$query = self::get_query();
$filter = [
SearchTable::QUERY->value => [
Operators::LIKE->value => "%{$_GET[SearchTable::QUERY->value]}%"
Operators::LIKE->value => "%{$query}%"
]
])
->select([
SearchTable::TITLE->value,
SearchTable::SUMMARY->value,
SearchTable::CATEGORY->value,
SearchTable::HREF->value
]);
];
if ($_GET[SearchTable::CATEGORY->value]) {
$filter[SearchTable::CATEGORY->value] = $_GET[SearchTable::CATEGORY->value];
}
$result = $result->where($filter);
} else {
new Response([], 400);
}
$result = $result->select([
SearchTable::ID->value,
SearchTable::TITLE->value,
SearchTable::SUMMARY->value,
SearchTable::CATEGORY->value,
SearchTable::HREF->value
]);
return $result->num_rows > 0
? new Response($result->fetch_all(MYSQLI_ASSOC))

73
endpoints/search/POST.php Normal file
View file

@ -0,0 +1,73 @@
<?php
use vlw\MySQL\Operators;
use Reflect\{Response, Path, Call};
use ReflectRules\{Ruleset, Rules, Type};
use VLW\API\Endpoints;
use VLW\Database\Database;
use const VLW\SEARCH_QUERY_MAX_LENGTH;
use VLW\Database\Tables\Search\{SearchTable, SearchCategoryEnum};
use VLW\Database\Tables\Work\{WorkTable, ActionsTable};
require_once Path::root("src/Consts.php");
require_once Path::root("src/API/Endpoints.php");
require_once Path::root("src/Database/Database.php");
require_once Path::root("src/Database/Tables/Work/Work.php");
require_once Path::root("src/Database/Tables/Work/Actions.php");
require_once Path::root("src/Database/Tables/Search/Search.php");
class POST_Search extends Database {
protected Ruleset $ruleset;
public function __construct() {
$this->ruleset = new Ruleset(strict: true);
$this->ruleset->validate_or_exit();
parent::__construct();
}
private static function truncate_string(string $input): string {
return substr($input, 0, SEARCH_QUERY_MAX_LENGTH);
}
private static function create_query(string $input): string {
preg_match_all("/[a-zA-Z0-9]+/", $input, $matches);
return self::truncate_string(strtolower(implode("", $matches[0])));
}
private function index_work(): void {
foreach ((new Call(Endpoints::WORK->value))->get()->output() as $result) {
$query = self::create_query(implode("", array_values($result)), 0, SEARCH_QUERY_MAX_LENGTH);
// Get actions related to current result
$actions = (new Call(Endpoints::WORK_ACTIONS->value))->params([
ActionsTable::REF_WORK_ID->value => $result[WorkTable::ID->value]
])->get()->output();
$this->db->for(SearchTable::NAME)->insert([
SearchTable::QUERY->value => $query,
SearchTable::ID->value => crc32($query),
SearchTable::TITLE->value => self::truncate_string($result[WorkTable::TITLE->value]),
SearchTable::SUMMARY->value => self::truncate_string($result[WorkTable::SUMMARY->value]),
SearchTable::CATEGORY->value => SearchCategoryEnum::WORK->name,
// Use first action as link for search result
SearchTable::HREF->value => $actions
? self::truncate_string($actions[0][ActionsTable::HREF->value])
: null
]);
}
}
public function main(): Response {
// Truncate existing cache
(new Call(Endpoints::SEARCH->value))->delete();
$this->index_work();
return $result->num_rows > 0
? new Response($result->fetch_all(MYSQLI_ASSOC))
: new Response([], 404);
}
}

View file

@ -54,4 +54,11 @@ section.start-search {
section.start-search svg {
width: 60px;
}
/* # Result */
section.result {
padding: var(--padding);
background-color: rgba(255, 255, 255, .1);
}

View file

@ -1,7 +1,7 @@
<?php
use const VLW\ICONS_DIR;
use VLW\Database\Models\Search\Search;
use const VLW\{ICONS_DIR, DEFAULT_BUTTON_ICON};
use VLW\Database\Tables\Search\{SearchTable, SearchCategoryEnum};
require_once VV::root("src/Consts.php");
@ -20,7 +20,7 @@
}
public function search(): array {
$results = parent::all([SearchTable::QUERY->value => self::get_query()]);
return parent::all([SearchTable::QUERY->value => self::get_query()]);
}
}
@ -30,7 +30,6 @@
<form>
<input name="<?= SearchTable::QUERY->value ?>" type="search" placeholder="search vlw.se..." value="<?= $search::get_query() ?>">
<select name="<?= SearchTable::CATEGORY->value ?>">
<option value="null">All</option>
<optgroup label="Categories">
@ -40,16 +39,30 @@
<?php endforeach; ?>
</optgroup>
</select>
<button type="submit" class="inline solid"><?= VV::embed(ICONS_DIR . "search.svg") ?></button>
</form>
</section>
<?php if (array_key_exists(SearchTable::QUERY->value, $_GET)): ?>
<section class="results">
</section>
<?php foreach ($search->search() as $result): ?>
<section class="result">
<?php if ($result->title()): ?>
<h2><?= $result->title() ?></h2>
<?php endif; ?>
<p><?= $result->summary() ?></p>
<?php if ($result->href()): ?>
<a href="<?= $result->href() ?>"><button class="inline">
<p>read more</p>
<?= VV::embed(DEFAULT_BUTTON_ICON) ?>
</button></a>
<?php endif; ?>
</section>
<?php endforeach; ?>
<?php else: ?>
<section class="start-search">
<?= VV::embed(ICONS_DIR . "search.svg") ?>

View file

@ -10,6 +10,13 @@
const ICONS_DIR = MEDIA_DIR . "icons/";
const DEFAULT_BUTTON_ICON = ICONS_DIR . "chevron.svg";
/**
* # Search
* Constants for the search API endpoint
*/
const SEARCH_QUERY_MAX_LENGTH = 2048;
const SEARCH_UPDATE_CACHE_PARM = "update";
/**
* # Timeline
* Constants related to the work timeline

View file

@ -25,18 +25,18 @@
}
public function title(): ?string {
return $this->get(Search::TITLE->value);
return $this->get(SearchTable::TITLE->value);
}
public function summary(): string {
return $this->get(Search::SUMMARY->value);
public function summary(): ?string {
return $this->get(SearchTable::SUMMARY->value);
}
public function category(): ?SearchCategoryEnum {
return SearchCategoryEnum::tryFromName($this->get(Search::CATEGORY->value));
return SearchCategoryEnum::tryFromName($this->get(SearchTable::CATEGORY->value));
}
public function href(): ?string {
return $this->get(Search::HREF->value);
return $this->get(SearchTable::HREF->value);
}
}