Skip to content

Commit

Permalink
Further hook improvements (#148)
Browse files Browse the repository at this point in the history
* Introduce the event object, update to hooks

* Bugfix and two additional filters

* Small content update

* Enhancement, fixed tests, wrote more hook tests

* Modified how we track event completeness

* Make PHPStan happy
  • Loading branch information
BelleNottelling authored Aug 9, 2024
1 parent 829aa72 commit 1e9e596
Show file tree
Hide file tree
Showing 12 changed files with 230 additions and 23 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ src/php_error.log
/reports
/src/Themes/Default/Assets/build
/tmp
/src/Plugins/Example
2 changes: 1 addition & 1 deletion phpunit.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.6/phpunit.xsd"
bootstrap="src/bootstrap.php" cacheResultFile=".phpunit.cache/test-results"
bootstrap="tests/Bootstrap.php" cacheResultFile=".phpunit.cache/test-results"
executionOrder="depends,defects" forceCoversAnnotation="false"
beStrictAboutCoversAnnotation="true" beStrictAboutOutputDuringTests="true"
beStrictAboutTodoAnnotatedTests="true" convertDeprecationsToExceptions="true" failOnRisky="true"
Expand Down
4 changes: 0 additions & 4 deletions rector.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

declare(strict_types=1);

use Rector\CodeQuality\Rector\Array_\CallableThisArrayToAnonymousFunctionRector;
use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\SetList;

Expand All @@ -22,7 +21,4 @@
SetList::STRICT_BOOLEANS,
SetList::CODE_QUALITY,
])
->withSkip([
CallableThisArrayToAnonymousFunctionRector::class,
])
->withPhpSets();
144 changes: 144 additions & 0 deletions src/AntCMS/Event.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
<?php

namespace AntCMS;

use DateTime;

class Event
{
private readonly string $associatedHook;
private readonly DateTime $firedAt;
private readonly float|int $hrStart;
private int|float $timeElapsed;

private int $paramUpdateCount = 0;
private int $paramReadCount = 0;
private int $lastCallback = 0;

/**
* @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)
{
if (!HookController::isRegistered($associatedHook)) {
throw new \Exception("Hook $associatedHook is not registered!");
}

$this->associatedHook = $associatedHook;
$this->firedAt = new DateTime();
$this->hrStart = hrtime(true);
}

/**
* Indicates if the hook has completed
*/
public function isDone(): bool
{
return $this->lastCallback === $this->totalCallbacks;
}

/**
* Called by the hook after each callback is complete.
* Tracks if the event is completed & fires 'onHookFireComplete' when needed.
*
* Callbacks should not call this function.
*
* @return Event
*/
public function next(): Event
{
if (!$this->isDone()) {
// We just completed a callback, increment the last callback number.
++$this->lastCallback;

// Check if that was the last callback & we are done
if ($this->isDone()) {
// Set the timing
$this->timeElapsed = hrtime(true) - $this->hrStart;

// Fire `onHookFireComplete`
if ($this->associatedHook !== 'onHookFireComplete') {
HookController::fire('onHookFireComplete', [
'name' => $this->associatedHook,
'firedAt' => $this->firedAt,
'timeElapsed' => $this->timeElapsed,
'parameterReadCount' => $this->paramReadCount,
'parameterUpdateCount' => $this->paramUpdateCount,
]);
}
}
} else {
// We shouldn't run into this situation, but it's non-fatal so only trigger a warning.
trigger_error("The 'next' event was called too many times for the '$this->associatedHook' event. Event timing may be inaccurate and 'onHookFireComplete' would have been fired too soon.", E_USER_WARNING);
}

return $this;
}

/**
* Indicates when the event was originally fired at
*/
public function firedAt(): DateTime
{
return $this->firedAt;
}

/**
* Returns the total time spent for this event in nanoseconds
*/
public function timeElapsed(): int|float
{
return $this->timeElapsed;
}

/**
* Gets the associated hook name
*/
public function getHookName(): string
{
return $this->associatedHook;
}

/**
* Gets the event parameters
*
* @return mixed[]
*/
public function getParameters(): array
{
$this->paramReadCount++;
return $this->parameters;
}

/**
* Updates the parameters
*
* @param mixed[] $parameters
*
* @return Event
*/
public function setParameters(array $parameters): Event
{
$this->parameters = $parameters;
$this->paramUpdateCount++;
return $this;
}

/**
* Returns the number of times the event parameters were read from
*/
public function getReadCount(): int
{
return $this->paramReadCount;
}

/**
* Returns the number of times the event parameters were updated.
*/
public function getUpdateCount(): int
{
return $this->paramUpdateCount;
}
}
17 changes: 15 additions & 2 deletions src/AntCMS/Hook.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,25 @@ public function __construct(string $name, public string $description)
*
* @param mixed[] $params An array of values to pass to the callbacks registered for this hook
*/
public function fire(array $params): void
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);

// Then fire each of the callbacks and update the event instance from each one.
foreach ($this->callbacks as $callback) {
call_user_func($callback, $params);
$newEvent = call_user_func($callback, $event);
if ($newEvent instanceof Event) {
$event = $newEvent;
}

// Mark that callback as done
$event->next();
}

return $event;
}

/**
Expand Down
9 changes: 4 additions & 5 deletions src/AntCMS/HookController.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public static function registerHook(string $name, string $description = ''): boo
{
if (self::isRegistered($name)) {
if ($description !== '') {
self::$hooks[$name]->$description = $description;
self::$hooks[$name]->description = $description;
}
return true;
}
Expand All @@ -44,12 +44,11 @@ public static function registerCallback(string $name, callable $callback): void
/**
* @param mixed[] $params
*/
public static function fire(string $name, array $params): void
public static function fire(string $name, array $params): Event
{
if (self::isRegistered($name)) {
self::$hooks[$name]->fire($params);
} else {
error_log("Hook '$name' is not registed and cannot be fired");
return self::$hooks[$name]->fire($params);
}
throw new \Exception("Hook '$name' is not registed and cannot be fired");
}
}
12 changes: 11 additions & 1 deletion src/AntCMS/Markdown.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ public static function parse(string $md, ?string $cacheKey = null): string
$cacheKey ??= Cache::createCacheKey($md, 'markdown');

return Cache::get($cacheKey, function (ItemInterface $item) use ($md): string {

// Fire the `onBeforeMarkdownParsed` event and use the potentially modified markdown content for parsing
$event = HookController::fire('onBeforeMarkdownParsed', ['markdown' => $md]);
$markdown = $event-> getParameters()['markdown'] ?? $md;

$config = Config::get();
$defaultAttributes = [];
$themeConfig = AntCMS::getThemeConfig();
Expand Down Expand Up @@ -54,7 +59,12 @@ public static function parse(string $md, ?string $cacheKey = null): string
$environment->addExtension(new DefaultAttributesExtension());

$markdownConverter = new MarkdownConverter($environment);
return $markdownConverter->convert($md)->__toString();

$parsed = $markdownConverter->convert($markdown)->__toString();

// Fire the `onAfterMarkdownParsed` hook and allow any updated HTML content to be used
$event = HookController::fire('onAfterMarkdownParsed', ['html' => $parsed]);
return $event->getParameters()['html'] ?? $parsed;
});
}
}
2 changes: 1 addition & 1 deletion src/AntCMS/TwigFilters.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public function getFilters(): array
{
return [
new TwigFilter('absUrl', $this->absUrl(...)),
new TwigFilter('markdown', $this->markdown(...)),
new TwigFilter('markdown', $this->markdown(...), ['is_safe' => ['html']]),
];
}

Expand Down
3 changes: 3 additions & 0 deletions src/Content/development/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ All plugins reside under the `Plugins` folder.
- When fired, hook callbacks will be fired in the order they were registered.
- You may register a callback before the hook itself has been registered.
- Registering a hook for a second time will simply update the description.
- Hook callbacks will be passed an "Event" object, containing the event parameters and assocaited info.
- Callbacks may modify the event and return the updated object which will then be used for any further callbacks.
- If a callback modifies hook parameters,it's up to the code that fired the event to read from the updated parameters.

---

Expand Down
3 changes: 3 additions & 0 deletions src/Plugins/System/Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ class Controller extends AbstractPlugin
'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',
'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',
];

public function __construct()
Expand Down
7 changes: 7 additions & 0 deletions tests/Bootstrap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

// Load the standard AntCMS Bootstrap file
require_once dirname(__DIR__) . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'bootstrap.php';

// Register plugins so hook tests can function correctly
AntCMS\PluginController::init();
49 changes: 40 additions & 9 deletions tests/HookTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,25 @@

class HookTest extends TestCase
{
public function hookCallback(array $params): void
// Validates the data provided by a hook is as it should be.
public function hookCallback(AntCMS\Event $event): void
{
$this->assertIsArray($params);
$this->assertArrayHasKey('test', $params);
$this->assertTrue($params['test'] === 'data');
$this->assertIsArray($event->getParameters());
$this->assertArrayHasKey('test', $event->getParameters());
$this->assertTrue($event->getParameters()['test'] === 'data');
$this->assertEquals('testHook', $event->getHookName());
}

// Modifies an event's parameters so we can then assert the updated parameters were recieved.
public function modifyHook(AntCMS\Event $event): AntCMS\Event
{
$event->setParameters(['Howdy!', 'How', "are", 'you?']);
return $event;
}

public function testHookNameAlllowed(): void
{
$result = HookController::registerHook('thisIs_Valid1234', '');
$result = HookController::registerHook('thisIs_Valid1234');
$this->assertIsBool($result);
$this->assertTrue($result);
}
Expand All @@ -23,19 +32,41 @@ public function testHookNameNotAllowed(): void
{
$this->expectException(Exception::class);
$this->expectExceptionMessage("The hook name 'thisIs NOT Valid1234' is invalid. Only a-z A-Z, 0-9, and _ are allowed to be in the hook name.");
HookController::registerHook('thisIs NOT Valid1234', '');
HookController::registerHook('thisIs NOT Valid1234');
}

public function testHookWorks(): void
{
$name = 'testHook';

$result = HookController::registerHook($name, '');
$result = HookController::registerHook($name);
$this->assertIsBool($result);
$this->assertTrue($result);

HookController::registerCallback($name, $this->hookCallback(...));
HookController::fire($name, ['test' => 'data']);
$event = HookController::fire($name, ['test' => 'data']);

$this->assertEquals(3, $event->getReadCount());
$this->assertEquals(0, $event->getUpdateCount());
$this->assertTrue($event->isDone());
$this->assertGreaterThan(0, $event->timeElapsed());
}

public function testHookParamUpdated(): void
{
$name = 'testHook';
$startData = ['test' => 'data'];
$expected = ['Howdy!', 'How', "are", 'you?'];

$result = HookController::registerHook($name);
$this->assertIsBool($result);
$this->assertTrue($result);

HookController::registerCallback($name, $this->modifyHook(...));
$event = HookController::fire($name, $startData);

$this->assertEquals($expected, $event->getParameters());
$this->assertEquals(1, $event->getUpdateCount());
}

public function testIsRegistered(): void
Expand All @@ -46,7 +77,7 @@ public function testIsRegistered(): void
$this->assertIsBool($result);
$this->assertFalse($result);

HookController::registerHook($name, '');
HookController::registerHook($name);

$result = HookController::isRegistered($name);
$this->assertIsBool($result);
Expand Down

0 comments on commit 1e9e596

Please sign in to comment.