diff --git a/src/AntCMS/AntCMS.php b/src/AntCMS/AntCMS.php index 9a7cf9b..c8da6ec 100644 --- a/src/AntCMS/AntCMS.php +++ b/src/AntCMS/AntCMS.php @@ -21,7 +21,7 @@ public function renderPage(?string $page = null): string $this->renderException("404"); } - HookController::fire('contentHit', ['contentUri' => $page]); + HookController::fire('onAfterContentHit', ['contentUri' => $page]); $themeConfig = self::getThemeConfig(); $params = [ diff --git a/src/AntCMS/AntYaml.php b/src/AntCMS/AntYaml.php index a94adf4..00e8643 100644 --- a/src/AntCMS/AntYaml.php +++ b/src/AntCMS/AntYaml.php @@ -45,7 +45,7 @@ public static function saveFile(string $path, array $data): bool } /** - * Parses a string containing YAML data and returns the content as an array. + * Parses a string containing YAML data and returns the content as an array. * @return mixed[] */ public static function parseYaml(string $yaml): array diff --git a/src/AntCMS/ApiController.php b/src/AntCMS/ApiController.php index 4a0e16e..0e37118 100644 --- a/src/AntCMS/ApiController.php +++ b/src/AntCMS/ApiController.php @@ -35,7 +35,7 @@ private function getApiCallData(string $plugin, string $method): array { // Some needed variable setup $url = rtrim(Flight::request()->url, '/'); - if($_GET !== []) { + if ($_GET !== []) { $query = '?' . http_build_query($_GET); $url = str_replace($query, '', $url); } @@ -91,7 +91,7 @@ private function call(string $type, string $plugin, string $method): void } $hookData = ['plugin' => ucfirst($plugin), 'method' => $method]; - HookController::fire('beforeApiCalled', $hookData); + HookController::fire('onBeforeApiCalled', $hookData); // Sanity checks passed, now actually process the request try { @@ -108,7 +108,7 @@ private function call(string $type, string $plugin, string $method): void } $hookData['response'] = $response; - HookController::fire('afterApiCalled', $hookData); + HookController::fire('onAfterApiCalled', $hookData); $this->sendResponse($response); } diff --git a/src/AntCMS/ApiResponse.php b/src/AntCMS/ApiResponse.php index b664f1a..b305fca 100644 --- a/src/AntCMS/ApiResponse.php +++ b/src/AntCMS/ApiResponse.php @@ -4,7 +4,7 @@ class ApiResponse { - public function __construct(private mixed $result, private readonly bool $error = false, private readonly int $code = 200, private string $message = '') + public function __construct(private readonly mixed $result, private readonly bool $error = false, private readonly int $code = 200, private readonly string $message = '') { } diff --git a/src/AntCMS/Cache.php b/src/AntCMS/Cache.php index 7b67af0..12ed124 100644 --- a/src/AntCMS/Cache.php +++ b/src/AntCMS/Cache.php @@ -18,9 +18,9 @@ class Cache /** * Configures the cache system by selecting and prioritizing specific cache adapters. - * + * * The `$allowed` array specifies which cache types to enable. Available options are 'apcu', 'php_file', and 'filesystem'. - * + * * @param string[] $allowed An array of allowed cache adapter names. */ public static function setup(array $allowed = []): void @@ -49,13 +49,13 @@ public static function setup(array $allowed = []): void /** * Retrieves a value from the cache. - * + * * If the value is not found in the cache, it executes the provided callable and stores the result for future retrieval. - * + * * @param string $key The unique identifier for the cached value. * @param callable|CallbackInterface $callable A function that returns the value to be cached. * @param ?float $beta (Optional) Controls the cache update strategy. See Symfony documentation for details. - * @param ?mixed[] &$metadata (Optional) Stores metadata about the cached item. + * @param ?mixed[] &$metadata (Optional) Stores metadata about the cached item. * @return mixed The cached value or the result of the callable if it's not found. */ public static function get(string $key, callable|CallbackInterface $callable, ?float $beta = null, ?array &$metadata = []): mixed @@ -65,7 +65,7 @@ public static function get(string $key, callable|CallbackInterface $callable, ?f /** * Prunes the cache. This removes stale entries that are no longer needed. - * + * * @return bool True if any items were pruned, false otherwise. */ public static function prune(): bool diff --git a/src/AntCMS/Event.php b/src/AntCMS/Event.php index d0a89f3..21a516f 100644 --- a/src/AntCMS/Event.php +++ b/src/AntCMS/Event.php @@ -14,13 +14,14 @@ class Event private int $paramUpdateCount = 0; private int $paramReadCount = 0; private int $lastCallback = 0; + private bool $defaultPrevented = false; /** * @param string $associatedHook The hook that this event is associated with. Hook must exist. * * @param mixed[] $parameters */ - public function __construct(string $associatedHook, private array $parameters, private readonly int $totalCallbacks) + public function __construct(string $associatedHook, private array $parameters, private readonly int $totalCallbacks, private readonly bool $preventable) { if (!HookController::isRegistered($associatedHook)) { throw new \Exception("Hook $associatedHook is not registered!"); @@ -127,7 +128,7 @@ public function setParameters(array $parameters): Event } /** - * Returns the number of times the event parameters were read from + * Returns the number of times the event parameters were read from. */ public function getReadCount(): int { @@ -141,4 +142,37 @@ public function getUpdateCount(): int { return $this->paramUpdateCount; } + + /** + * Indicates if the default behavior for an event is preventable. + */ + public function isDefaultPreventable(): bool + { + return $this->preventable; + } + + /** + * Indicates if the default behavior for an event is prevented. + */ + public function isDefaultPrevented(): bool + { + return $this->defaultPrevented; + } + + /** + * Sets a flag for the default behavior of this event to be prevented. + * Not all events can be prevented. Triggers a non-fatal error if the event's default behavior is not preventable. + * + * @return Event + */ + public function preventDefault(): Event + { + if (!$this->isDefaultPreventable()) { + trigger_error("The default behavior for the `$this->associatedHook` hook cannot be prevented."); + } else { + $this->defaultPrevented = true; + } + + return $this; + } } diff --git a/src/AntCMS/Hook.php b/src/AntCMS/Hook.php index d3a6253..c0e811a 100644 --- a/src/AntCMS/Hook.php +++ b/src/AntCMS/Hook.php @@ -20,8 +20,9 @@ class Hook * * @param string $name The name of the hook * @param string $description A description of this hook + * @param bool $isDefaultPreventable Marks if the default behavior for a hook can be prevented. */ - public function __construct(string $name, public string $description) + public function __construct(string $name, public string $description, private readonly bool $isDefaultPreventable = false) { if (preg_match('/^\w+$/', $name) === 0 || preg_match('/^\w+$/', $name) === false) { throw new \Exception("The hook name '$name' is invalid. Only a-z A-Z, 0-9, and _ are allowed to be in the hook name."); @@ -40,7 +41,7 @@ public function fire(array $params): Event $this->timesFired++; // Create the new event object with the originally provided parameters - $event = new Event($this->name, $params, $this->registeredCallbacks); + $event = new Event($this->name, $params, $this->registeredCallbacks, $this->isDefaultPreventable); // Then fire each of the callbacks and update the event instance from each one. foreach ($this->callbacks as $callback) { diff --git a/src/AntCMS/HookController.php b/src/AntCMS/HookController.php index 4d7053b..7e1895e 100644 --- a/src/AntCMS/HookController.php +++ b/src/AntCMS/HookController.php @@ -20,7 +20,7 @@ public static function isRegistered(string $name): bool return array_key_exists($name, self::$hooks); } - public static function registerHook(string $name, string $description = ''): bool + public static function registerHook(string $name, string $description = '', bool $isDefaultPreventable = false): bool { if (self::isRegistered($name)) { if ($description !== '') { @@ -29,7 +29,7 @@ public static function registerHook(string $name, string $description = ''): boo return true; } - self::$hooks[$name] = new Hook($name, $description); + self::$hooks[$name] = new Hook($name, $description, $isDefaultPreventable); return true; } @@ -42,9 +42,9 @@ public static function registerCallback(string $name, callable $callback): void } /** - * @param mixed[] $params + * @param mixed[] $params (Optional) */ - public static function fire(string $name, array $params): Event + public static function fire(string $name, array $params = []): Event { if (self::isRegistered($name)) { return self::$hooks[$name]->fire($params); diff --git a/src/AntCMS/Pages.php b/src/AntCMS/Pages.php index 026b18d..e4ef6fe 100644 --- a/src/AntCMS/Pages.php +++ b/src/AntCMS/Pages.php @@ -109,10 +109,10 @@ private static function buildList(string $path = PATH_CONTENT): array if (isset($directoryMeta['pageOrder'][$a]) && isset($directoryMeta['pageOrder'][$b])) { return $directoryMeta['pageOrder'][$a] > $directoryMeta['pageOrder'][$b] ? 1 : -1; } - if(isset($directoryMeta['pageOrder'][$a]) && !isset($directoryMeta['pageOrder'][$b])) { + if (isset($directoryMeta['pageOrder'][$a]) && !isset($directoryMeta['pageOrder'][$b])) { return -1; } - if(!isset($directoryMeta['pageOrder'][$a]) && isset($directoryMeta['pageOrder'][$b])) { + if (!isset($directoryMeta['pageOrder'][$a]) && isset($directoryMeta['pageOrder'][$b])) { return 1; } // Ensure index items come first diff --git a/src/AntCMS/PluginController.php b/src/AntCMS/PluginController.php index c92f0e1..9a90da9 100644 --- a/src/AntCMS/PluginController.php +++ b/src/AntCMS/PluginController.php @@ -50,6 +50,8 @@ public static function init(): void Twig::addLoaderPath($templateDir); } } + + HookController::fire('onAfterPluginsInit'); } /** diff --git a/src/AntCMS/Tools.php b/src/AntCMS/Tools.php index 66ccfcf..89f86ec 100644 --- a/src/AntCMS/Tools.php +++ b/src/AntCMS/Tools.php @@ -2,6 +2,7 @@ namespace AntCMS; +use Flight; use HostByBelle\CompressionBuffer; use Symfony\Contracts\Cache\ItemInterface; @@ -257,7 +258,7 @@ public static function getPerformanceMetrics(): array ]; } - private static function createDebugLogLine(string $wording, bool|string $value): string + private static function createDebugLogLine(string $wording, bool|string|int $value): string { if (is_bool($value)) { $value = $value ? "enabled" : "disabled"; @@ -291,6 +292,7 @@ public static function buildDebugInfo(): string $result .= self::createDebugLogLine('This page was compressed with', $method); } + $result .= self::createDebugLogLine('Page output size', Flight::response()->getContentLength()); $result .= self::createDebugLogLine('Asset compression', COMPRESS_TEXT_ASSETS); $result .= self::createDebugLogLine('Image compression', COMPRESS_IMAGES); $result .= self::createDebugLogLine('PHP version', PHP_VERSION); diff --git a/src/Plugins/Robotstxt/Controller.php b/src/Plugins/Robotstxt/Controller.php index 7f95986..9a17584 100644 --- a/src/Plugins/Robotstxt/Controller.php +++ b/src/Plugins/Robotstxt/Controller.php @@ -6,7 +6,6 @@ use AntCMS\Config; use AntCMS\PluginController; use AntCMS\Tools; - use Flight; class Controller extends AbstractPlugin diff --git a/src/Plugins/System/Controller.php b/src/Plugins/System/Controller.php index 0165d5c..10c2777 100644 --- a/src/Plugins/System/Controller.php +++ b/src/Plugins/System/Controller.php @@ -3,6 +3,7 @@ namespace AntCMS\Plugins\System; use AntCMS\AbstractPlugin; +use AntCMS\AntCMS; use AntCMS\HookController; class Controller extends AbstractPlugin @@ -11,13 +12,15 @@ class Controller extends AbstractPlugin * @var array */ private array $hooks = [ - 'contentHit' => 'This is fired when markdown content is accessed. The URI will be passed in the data.', - 'performanceMetricsBuilt' => 'When fired, this event contains all performance metrics AntCMS was able to collect on a request. These are more complete & accurate than the metrics shown on the bottom of the screen.', - 'beforeApiCalled' => 'This event is fired before an API endpoint is called', - 'afterApiCalled' => 'This event is fired after an API endpoint is called and the response is available', + 'onAfterContentHit' => 'This is fired when markdown content is accessed. The URI will be passed in the data.', + 'onAfterPerformanceMetricsBuilt' => 'When fired, this event contains all performance metrics AntCMS was able to collect on a request. These are more complete & accurate than the metrics shown on the bottom of the screen.', + 'onBeforeApiCalled' => 'This event is fired before an API endpoint is called.', + 'onAfterApiCalled' => 'This event is fired after an API endpoint is called and the response is available.', 'onHookFireComplete' => 'This event is fired when others have completed. The data provided will include the hook name, timing data, and parameter read / update statistics.', - 'onBeforeMarkdownParsed' => 'This event is fired before markdown is converted, allowing for pre-processing before the markdown is run through the parser', - 'onAfterMarkdownParsed' => 'This is fired after markdown is converted, allowing you to modify generated markdown content', + 'onBeforeMarkdownParsed' => 'This event is fired before markdown is converted, allowing for pre-processing before the markdown is run through the parser.', + 'onAfterMarkdownParsed' => 'This is fired after markdown is converted, allowing you to modify generated markdown content.', + 'onAfterPluginsInit' => 'This event is fired after all plugins have been initialized.', + 'onBeforeOutputFlushed' => 'This event is fired right before the generated response is finalized (compressed) and sent to the browser. No later chances to modify the output buffer exist.', ]; public function __construct() @@ -27,6 +30,16 @@ public function __construct() HookController::registerHook($name, $description); } + HookController::registerCallback('onBeforeOutputFlushed', $this->appendDebugInfo(...)); + $this->addDisallow('/api/*'); } + + private function appendDebugInfo(\AntCMS\Event $event): \AntCMS\Event + { + $params = $event->getParameters(); + $params['output'] = str_replace('', \AntCMS\Tools::buildDebugInfo(), $params['output'] ?? ''); + $event->setParameters($params); + return $event; + } } diff --git a/src/index.php b/src/index.php index 1e7ce8c..7575a45 100644 --- a/src/index.php +++ b/src/index.php @@ -17,8 +17,11 @@ $AntCMS = new AntCMS(); -// Add a response body callback to display debug info -Flight::response()->addResponseBodyCallback(fn ($body): string => str_replace('', Tools::buildDebugInfo(), $body)); +// Use hooks to perform any final changes to the output buffer before compressing and sending it +Flight::response()->addResponseBodyCallback(function (string $body): string { + $event = HookController::fire('onBeforeOutputFlushed', ['output' => $body]); + return $event->getParameters()['output']; +}); // Setup CompressionBuffer & enable it in Flight CompressionBuffer::setUp(true, false, [Flight::response(), 'header']); @@ -27,7 +30,7 @@ } Flight::response()->addResponseBodyCallback(function ($body) { - HookController::fire('performanceMetricsBuilt', tools::getPerformanceMetrics()); + HookController::fire('onAfterPerformanceMetricsBuilt', tools::getPerformanceMetrics()); return $body; });