feat(demo): add story 1 — Sorano: Rock and Time

This commit is contained in:
2026-06-20 21:19:57 +02:00
parent 42ed59a6b3
commit 8f87155c1d
5508 changed files with 1595740 additions and 124 deletions
@@ -0,0 +1,258 @@
<?php
declare(strict_types=1);
namespace Grav\Plugin\Api\Webhooks;
use Grav\Common\Grav;
use RocketTheme\Toolbox\File\YamlFile;
class WebhookManager
{
private string $storagePath;
private string $deliveryPath;
private ?array $webhooksCache = null;
public function __construct()
{
$grav = Grav::instance();
$this->storagePath = $grav['locator']->findResource('user://data/api', true, true);
$this->deliveryPath = $this->storagePath . '/webhook-deliveries';
}
/**
* Get all webhooks.
*
* @return array
*/
public function getAll(): array
{
return $this->load();
}
/**
* Get a webhook by ID.
*/
public function get(string $id): ?array
{
$webhooks = $this->load();
foreach ($webhooks as $webhook) {
if ($webhook['id'] === $id) {
return $webhook;
}
}
return null;
}
/**
* Create a new webhook.
*/
public function create(array $data): array
{
$webhook = [
'id' => 'wh_' . bin2hex(random_bytes(12)),
'url' => $data['url'],
'secret' => 'whsec_' . bin2hex(random_bytes(24)),
'events' => $data['events'] ?? ['*'],
'enabled' => $data['enabled'] ?? true,
'headers' => $data['headers'] ?? [],
'created' => time(),
'failure_count' => 0,
];
$webhooks = $this->load();
$webhooks[] = $webhook;
$this->save($webhooks);
return $webhook;
}
/**
* Update a webhook.
*/
public function update(string $id, array $data): ?array
{
$webhooks = $this->load();
foreach ($webhooks as &$webhook) {
if ($webhook['id'] === $id) {
if (isset($data['url'])) {
$webhook['url'] = $data['url'];
}
if (isset($data['events'])) {
$webhook['events'] = $data['events'];
}
if (isset($data['enabled'])) {
$webhook['enabled'] = (bool) $data['enabled'];
}
if (isset($data['headers'])) {
$webhook['headers'] = $data['headers'];
}
$this->save($webhooks);
return $webhook;
}
}
return null;
}
/**
* Delete a webhook.
*/
public function delete(string $id): bool
{
$webhooks = $this->load();
$filtered = array_values(array_filter($webhooks, fn($w) => $w['id'] !== $id));
if (count($filtered) === count($webhooks)) {
return false;
}
$this->save($filtered);
// Clean up delivery logs
$deliveryDir = $this->deliveryPath . '/' . $id;
if (is_dir($deliveryDir)) {
$files = glob($deliveryDir . '/*.yaml');
foreach ($files as $file) {
@unlink($file);
}
@rmdir($deliveryDir);
}
return true;
}
/**
* Record a delivery log entry.
*/
public function recordDelivery(string $webhookId, array $delivery): void
{
$dir = $this->deliveryPath . '/' . $webhookId;
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
$file = $dir . '/' . $delivery['id'] . '.yaml';
$yamlFile = YamlFile::instance($file);
$yamlFile->content($delivery);
$yamlFile->save();
// Keep only last 50 deliveries
$this->pruneDeliveries($webhookId, 50);
}
/**
* Get delivery history for a webhook.
*/
public function getDeliveries(string $webhookId, int $limit = 20, int $offset = 0): array
{
$dir = $this->deliveryPath . '/' . $webhookId;
if (!is_dir($dir)) {
return ['deliveries' => [], 'total' => 0];
}
$files = glob($dir . '/*.yaml');
// Sort by modification time descending
usort($files, fn($a, $b) => filemtime($b) <=> filemtime($a));
$total = count($files);
$slice = array_slice($files, $offset, $limit);
$deliveries = [];
foreach ($slice as $file) {
$yamlFile = YamlFile::instance($file);
$deliveries[] = $yamlFile->content();
}
return ['deliveries' => $deliveries, 'total' => $total];
}
/**
* Get webhooks matching a specific event.
*/
public function getForEvent(string $event): array
{
$webhooks = $this->load();
return array_filter($webhooks, function ($webhook) use ($event) {
if (!($webhook['enabled'] ?? true)) {
return false;
}
$events = $webhook['events'] ?? ['*'];
return in_array('*', $events, true) || in_array($event, $events, true);
});
}
/**
* Increment failure count and auto-disable if threshold reached.
*/
public function recordFailure(string $id): void
{
$webhooks = $this->load();
foreach ($webhooks as &$webhook) {
if ($webhook['id'] === $id) {
$webhook['failure_count'] = ($webhook['failure_count'] ?? 0) + 1;
if ($webhook['failure_count'] >= 5) {
$webhook['enabled'] = false;
$webhook['disabled_reason'] = 'Auto-disabled after 5 consecutive failures';
}
break;
}
}
$this->save($webhooks);
}
/**
* Reset failure count on successful delivery.
*/
public function resetFailureCount(string $id): void
{
$webhooks = $this->load();
foreach ($webhooks as &$webhook) {
if ($webhook['id'] === $id) {
$webhook['failure_count'] = 0;
unset($webhook['disabled_reason']);
break;
}
}
$this->save($webhooks);
}
private function load(): array
{
if ($this->webhooksCache !== null) {
return $this->webhooksCache;
}
$file = YamlFile::instance($this->storagePath . '/webhooks.yaml');
$content = $file->content();
$this->webhooksCache = $content['webhooks'] ?? [];
return $this->webhooksCache;
}
private function save(array $webhooks): void
{
$file = YamlFile::instance($this->storagePath . '/webhooks.yaml');
$file->content(['webhooks' => array_values($webhooks)]);
$file->save();
$this->webhooksCache = array_values($webhooks);
}
private function pruneDeliveries(string $webhookId, int $keep): void
{
$dir = $this->deliveryPath . '/' . $webhookId;
if (!is_dir($dir)) {
return;
}
$files = glob($dir . '/*.yaml');
usort($files, fn($a, $b) => filemtime($b) <=> filemtime($a));
$toDelete = array_slice($files, $keep);
foreach ($toDelete as $file) {
@unlink($file);
}
}
}