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 <?php
use vlw\MySQL\Operators; use vlw\MySQL\Operators;
use Reflect\{Response, Path}; use Reflect\{Response, Path, Call};
use ReflectRules\{Ruleset, Rules, Type}; use ReflectRules\{Ruleset, Rules, Type};
use VLW\API\Endpoints;
use VLW\Database\Database; 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/Database.php");
require_once Path::root("src/Database/Tables/Search/Search.php"); require_once Path::root("src/Database/Tables/Search/Search.php");
@ -18,10 +22,24 @@
$this->ruleset->GET([ $this->ruleset->GET([
(new Rules(SearchTable::QUERY->value)) (new Rules(SearchTable::QUERY->value))
->required()
->type(Type::STRING) ->type(Type::STRING)
->min(2) ->min(2)
->max(parent::SIZE_VARCHAR) ->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(); $this->ruleset->validate_or_exit();
@ -29,20 +47,47 @@
parent::__construct(); 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 { public function main(): Response {
$result = $this->db // Freshen cache if update flag is set
->for(SearchTable::NAME) if ($_GET[SEARCH_UPDATE_CACHE_PARM]) {
->where([ (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 => [ SearchTable::QUERY->value => [
Operators::LIKE->value => "%{$_GET[SearchTable::QUERY->value]}%" Operators::LIKE->value => "%{$query}%"
] ]
]) ];
->select([
SearchTable::TITLE->value, if ($_GET[SearchTable::CATEGORY->value]) {
SearchTable::SUMMARY->value, $filter[SearchTable::CATEGORY->value] = $_GET[SearchTable::CATEGORY->value];
SearchTable::CATEGORY->value, }
SearchTable::HREF->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 return $result->num_rows > 0
? new Response($result->fetch_all(MYSQLI_ASSOC)) ? 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 { section.start-search svg {
width: 60px; width: 60px;
}
/* # Result */
section.result {
padding: var(--padding);
background-color: rgba(255, 255, 255, .1);
} }

View file

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

View file

@ -10,6 +10,13 @@
const ICONS_DIR = MEDIA_DIR . "icons/"; const ICONS_DIR = MEDIA_DIR . "icons/";
const DEFAULT_BUTTON_ICON = ICONS_DIR . "chevron.svg"; 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 * # Timeline
* Constants related to the work timeline * Constants related to the work timeline

View file

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