Plugin Annotations & Plugin Context

The Festi Framework provides a dedicated workbench layer (separate package: festi-workbench) responsible for reflection and codebase analysis of plugin method annotations. This layer parses and synchronizes metadata into the system, allowing system plugins to register URL rules, areas, sections, and permissions based on plugin definitions.

This package is intended for project preparation and compilation steps rather than runtime execution. It analyzes annotations and compiles the necessary configuration ahead of time, reducing runtime overhead.

Metadata declarations come in two forms — PHP 8 attributes and PHPDoc tags. The framework ships three attribute classes in core\plugin\attribute (AreaAttribute, UrlRouteAttribute, SectionAttribute) that mirror the historical @area / @urlRule / @section tags. Prefer attributes for new code; PHPDoc remains supported for existing plugins. The workbench parses both and merges them into the same metadata.

This workbench package is also intended as a foundation for building IDE extensions (e.g. PHPStorm, Visual Studio Code) for autocompletion, validation, navigation, and developer tooling using the same metadata.

  • When it runs: At plugin install or save in the Plugins UI, not on every request. Annotations are not applied on the fly; a sync step is required after changes.

PluginContext

Role: Represents a single plugin’s identity and path. It is the context given to PluginAnnotations when parsing that plugin.

Where it’s used: In the workbench when iterating over plugins (e.g. during install/sync). You construct a context with the plugin name and its base path; the workbench then uses it to locate the plugin class and pass it to PluginAnnotations.

What it provides: Plugin name, base path, system-flag, version, and lazy access to the plugin instance. See the class PHPDoc in festi-workbench for the full API.

PluginAnnotations

Role: Parses metadata from a plugin’s methods — both PHP 8 attributes and PHPDoc tags — and can sync the result into an ISystemObject (e.g. the system plugin object that holds URL rules, areas, sections). Both forms feed into the same result; when both are present on the same method, attributes take precedence on a per-annotation-name basis.

Where it’s used: In the workbench during the same install/sync flow. You create it with a PluginContext, call parse() (optionally with extra annotation names or attribute classes for plugins that define custom ones), then call sync() with the target system object. The framework’s Workbench wires this so that adding or saving a plugin in the Plugins UI triggers parse + sync.

What it provides:

  • Metadata declarations (what you can put on plugin methods):
  • #[AreaAttribute('backend')] / @area backend — URL area(s) for the method (e.g. default, admin).
  • #[UrlRouteAttribute('~^/foo/$~')] / @urlRule ~^/foo/$~ — Regex pattern for URL routing.
  • #[SectionAttribute(PermissionConstants::FOO)] / @section <sectionName>|<mask> — Section name and permission mask. In PHPDoc, mask is one of read (2) / write (4) / exec (6); default is exec if omitted. The attribute form uses the numeric permission codes directly through SectionAttribute::MASK_READ / MASK_WRITE / MASK_EXEC, so the two stay in sync.

  • Workflow: parse() runs reflection and returns a PluginMetadata (annotations + per-method MethodInfo). sync() takes that result and pushes it into the given ISystemObject (e.g. registering areas, URL rules, sections). You must call parse() before sync().

  • Extension points: Plugins (e.g. RPC, MCPServer) hook BeforePluginAnnotationsParseEvent to register their own doc-tag names AND attribute classes for parsing. The result then exposes which methods carry which annotations and their values — useful for custom tooling, RPC method discovery, or code generation.

For method signatures, return types, and detailed behaviour of PluginMetadata and MethodInfo, use the generated API docs or the PHPDoc on the classes in festi-workbench.

PHP 8 Attributes (preferred)

The three attribute classes are pure value objects — they carry data only; parsing and sync are the Workbench's job.

Classes (namespace core\plugin\attribute):

  • AreaAttribute(string $name) — mirrors @area. The URL area (e.g. backend, api, rpc).
  • UrlRouteAttribute(string $pattern) — mirrors @urlRule. The regex pattern for URL matching.
  • SectionAttribute(string $name, string $mask = SectionAttribute::MASK_EXEC) — mirrors @section. The permission section identifier with an optional permission mask. Exactly one per method (not repeatable). Plugins that need multi-section OR-logic stay on PHPDoc.
  • SectionAttribute::MASK_READ'2'
  • SectionAttribute::MASK_WRITE'4'
  • SectionAttribute::MASK_EXEC'6' (default)

All three target Attribute::TARGET_METHOD only.

Why prefer attributes:

  • FQN / constant resolution for free. SectionAttribute(AttendanceSections::COMMON) carries the constant's actual value ("attendance_common"); PHP resolves it at attribute-construction time.
  • IDE & refactor support. Renaming a class or constant updates every attribute usage; PHPDoc strings are silently invisible to refactor tooling.
  • Static analysis. phan / phpstan understand attribute types; doc-tag values are opaque to them.

Example (preferred form):

use core\plugin\attribute\AreaAttribute;
use core\plugin\attribute\SectionAttribute;
use core\plugin\attribute\UrlRouteAttribute;

#[UrlRouteAttribute('~^/company/([0-9]+)/attendance/$~')]
#[SectionAttribute(AttendanceSections::COMMON)]
#[AreaAttribute('backend')]
public function onDisplayBySchool(Response &$response, int $idSchool): bool
{
    // ...
}

Specifying a non-default permission mask:

use core\plugin\attribute\AreaAttribute;
use core\plugin\attribute\SectionAttribute;
use core\plugin\attribute\UrlRouteAttribute;

#[UrlRouteAttribute('~^/company/([0-9]+)/attendance/report/$~')]
#[SectionAttribute(AttendanceSections::COMMON, SectionAttribute::MASK_READ)]
#[AreaAttribute('backend')]
public function onAjaxGetReport(Response &$response, int $idSchool): void
{
    // ...
}

When the mask argument is omitted, MASK_EXEC is used — the same default as the omitted-mask doc-tag form.

Legacy form (PHPDoc):

/**
 * @urlRule ~^/company/([0-9]+)/attendance/$~
 * @section AttendanceSections::COMMON
 * @area backend
 */
public function onDisplayBySchool(Response &$response, int $idSchool): bool
{
    // ...
}

The workbench parser resolves Class::CONST references in doc-tag values against the source file's use / namespace declarations, so the legacy form lands the same resolved ident as the attribute form. Doc-tag @return / @param short class names are similarly rewritten to FQNs against the file's use aliases.

Coexistence rules:

  • New code should use attributes. PHPDoc remains supported for backward compatibility.
  • When the same method declares both forms for the same annotation name, attributes take precedence; orthogonal annotations from each source coexist on the same MethodInfo.
  • Plugins already using multi-@section OR-logic must stay on PHPDoc — SectionAttribute is deliberately single-valued.