From e94fb365ff6d61443e85f95c554c1a0ea131f76d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 08:01:15 +0000 Subject: [PATCH] fix(demos): repair the AOP switch and split the demo into backend and frontend With AOP switched off the page died halfway: resolving the source file of a showcase called MagicConstantTransformer::resolveFileName(), which needs the aspect kernel that is not booted in this mode. The fatal error cut the page before the Bootstrap bundle and the switch script, so neither the dropdown menus nor the switch reacted any more. The demo is now split: - index.php is a small front controller: it stores the switch state and runs the selected showcase - Demo\Showcase holds the showcases (catalog, runner, result, source code); only woven classes (Proxy) have their file name resolved to the original - templates/page.php is the HTML, assets/demo.css and assets/demo.js the styles and the behaviour of the page The AOP switch is a form posted to the server, which keeps the state in a cookie and redirects back, so it works without JavaScript. The navigation is a plain sidebar (a drawer on small screens) instead of Bootstrap dropdowns. The new design has light and dark themes, a landing page with a card per example, the output in a terminal panel, the usage code, the aspect and the class in source tabs highlighted with highlight.js, and copy buttons. The CI smoke test lists the showcases from the catalog and renders every page with AOP on and off, checking that it is rendered completely. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TkXmBX6yR2tyL4TqAzphcm --- .github/workflows/phpunit.yml | 28 +- demos/Demo/Highlighter.php | 31 - demos/Demo/Showcase/Showcase.php | 72 ++ demos/Demo/Showcase/ShowcaseCatalog.php | 295 +++++++ demos/Demo/Showcase/ShowcaseResult.php | 45 ++ demos/Demo/Showcase/SourceCode.php | 89 +++ demos/assets/demo.css | 992 ++++++++++++++++++++++++ demos/assets/demo.js | 115 +++ demos/index.php | 298 +------ demos/templates/page.php | 205 +++++ phpstan-baseline-demos.php | 12 +- 11 files changed, 1860 insertions(+), 322 deletions(-) delete mode 100644 demos/Demo/Highlighter.php create mode 100644 demos/Demo/Showcase/Showcase.php create mode 100644 demos/Demo/Showcase/ShowcaseCatalog.php create mode 100644 demos/Demo/Showcase/ShowcaseResult.php create mode 100644 demos/Demo/Showcase/SourceCode.php create mode 100644 demos/assets/demo.css create mode 100644 demos/assets/demo.js create mode 100644 demos/templates/page.php diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 5d24d4775..94b42d8d9 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -156,14 +156,24 @@ jobs: - name: "Install highest dependencies" run: "composer update --no-interaction --no-progress" - - name: "Run every demo showcase through the weaver" + - name: "Run every demo showcase with and without the weaver" run: | - for showcase in $(grep -o "case '[a-z-]*'" demos/index.php | cut -d"'" -f2); do - echo "::group::${showcase}" - php -r "\$_GET['showcase'] = '${showcase}'; include 'demos/index.php';" > "demo-${showcase}.html" - if grep -qiE '(fatal error|uncaught|warning|deprecated|notice)' "demo-${showcase}.html"; then - grep -iE '(fatal error|uncaught|warning|deprecated|notice)' "demo-${showcase}.html" - exit 1 - fi - echo "::endgroup::" + showcases=$(php -r 'require "vendor/autoload.php"; foreach ((new Demo\Showcase\ShowcaseCatalog())->all() as $showcase) echo $showcase->id, PHP_EOL;') + for aop in on off; do + for showcase in "" ${showcases}; do + page="demo-${showcase:-index}-aop-${aop}.html" + echo "::group::${showcase:-index} (AOP ${aop})" + php -r "\$_COOKIE['aop'] = '${aop}'; \$_GET['showcase'] = '${showcase}'; include 'demos/index.php';" > "${page}" + if grep -qiE '(fatal error|uncaught|warning|deprecated|notice)' "${page}"; then + grep -iE '(fatal error|uncaught|warning|deprecated|notice)' "${page}" + exit 1 + fi + # A page cut short leaves the navigation and the AOP switch without their script + if ! grep -q '' "${page}"; then + echo "The page is not rendered completely" + tail -n 20 "${page}" + exit 1 + fi + echo "::endgroup::" + done done diff --git a/demos/Demo/Highlighter.php b/demos/Demo/Highlighter.php deleted file mode 100644 index 69dcb88e4..000000000 --- a/demos/Demo/Highlighter.php +++ /dev/null @@ -1,31 +0,0 @@ - - * - * This source file is subject to the license that is bundled - * with this source code in the file LICENSE. - */ - -namespace Demo; - -/** - * Highlighter utility class - */ -final class Highlighter -{ - /** - * Highlighter with built-in check for list of disabled function (Google AppEngine) - */ - public static function highlight(string $file): void - { - if (function_exists('highlight_file')) { - highlight_file($file); - } else { - echo '
' . htmlspecialchars(file_get_contents($file)) . '
'; - } - } -} diff --git a/demos/Demo/Showcase/Showcase.php b/demos/Demo/Showcase/Showcase.php new file mode 100644 index 000000000..8562a4886 --- /dev/null +++ b/demos/Demo/Showcase/Showcase.php @@ -0,0 +1,72 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Demo\Showcase; + +use Closure; +use Go\Aop\Aspect; +use ReflectionClass; +use ReflectionObject; + +/** + * One example of the demo: an aspect and the scenario that shows it in action + */ +final readonly class Showcase +{ + /** + * @param string $id Identifier used in the URL + * @param string $group Navigation group of the showcase + * @param string $title Short title for the navigation + * @param string $summary One sentence about what the aspect does + * @param class-string $aspect Aspect demonstrated by the showcase + * @param Closure(): (object|null) $scenario Example code, returns the object whose class is shown + */ + public function __construct( + public string $id, + public string $group, + public string $title, + public string $summary, + public string $aspect, + private Closure $scenario, + ) {} + + /** + * Short name of the aspect class + */ + public function aspectName(): string + { + return new ReflectionClass($this->aspect)->getShortName(); + } + + /** + * Runs the scenario and collects its output together with the source code involved + */ + public function run(): ShowcaseResult + { + $startedAt = hrtime(true); + ob_start(); + try { + $subject = ($this->scenario)(); + } finally { + $output = (string) ob_get_clean(); + } + $elapsedMs = (hrtime(true) - $startedAt) / 1e6; + + return new ShowcaseResult( + output: $output, + elapsedMs: $elapsedMs, + usage: SourceCode::ofClosure($this->scenario), + aspect: SourceCode::ofClass(new ReflectionClass($this->aspect)), + subject: $subject !== null ? SourceCode::ofClass(new ReflectionObject($subject)) : null, + ); + } +} diff --git a/demos/Demo/Showcase/ShowcaseCatalog.php b/demos/Demo/Showcase/ShowcaseCatalog.php new file mode 100644 index 000000000..812d10f54 --- /dev/null +++ b/demos/Demo/Showcase/ShowcaseCatalog.php @@ -0,0 +1,295 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Demo\Showcase; + +use Demo\Aspect\CachingAspect; +use Demo\Aspect\DynamicMethodsAspect; +use Demo\Aspect\EnumInterceptorAspect; +use Demo\Aspect\ErrorMonitoringAspect; +use Demo\Aspect\FluentInterfaceAspect; +use Demo\Aspect\FunctionInterceptorAspect; +use Demo\Aspect\HealthyLiveAspect; +use Demo\Aspect\IntroductionAspect; +use Demo\Aspect\LoggingAspect; +use Demo\Aspect\PrivateMethodAspect; +use Demo\Aspect\PropertyHooksAspect; +use Demo\Aspect\PropertyInterceptorAspect; +use Demo\Example\CacheableDemo; +use Demo\Example\DynamicMethodsDemo; +use Demo\Example\FunctionDemo; +use Demo\Example\HumanDemo; +use Demo\Example\IntroductionDemo; +use Demo\Example\LoggingDemo; +use Demo\Example\OrderProcessorDemo; +use Demo\Example\OrderStatus; +use Demo\Example\PaymentDemo; +use Demo\Example\ProductDemo; +use Demo\Example\PropertyDemo; +use Demo\Example\UserFluentDemo; +use Go\Aop\Proxy; +use InvalidArgumentException; + +/** + * List of all showcases of the demo + * + * The body of every scenario is shown on the page as the usage example, keep it short and readable. + */ +final class ShowcaseCatalog +{ + /** + * @var array + */ + private array $showcases = []; + + public function __construct() + { + foreach (self::define() as $showcase) { + $this->showcases[$showcase->id] = $showcase; + } + } + + /** + * @return list + */ + public function all(): array + { + return array_values($this->showcases); + } + + /** + * @return array> Showcases indexed by the name of their group + */ + public function groups(): array + { + $groups = []; + foreach ($this->showcases as $showcase) { + $groups[$showcase->group][] = $showcase; + } + + return $groups; + } + + public function find(string $id): ?Showcase + { + return $this->showcases[$id] ?? null; + } + + /** + * @return iterable + */ + private static function define(): iterable + { + yield new Showcase( + id: 'loggable', + group: 'Advices', + title: 'Logging', + summary: 'A Before advice logs every call of the methods marked with #[Loggable], protected and static ones included.', + aspect: LoggingAspect::class, + scenario: static function (): LoggingDemo { + $example = new LoggingDemo(); + $example->execute('LoggingTask'); // Logging for dynamic methods + LoggingDemo::runByName('StaticTask'); // Logging for static methods + + return $example; + }, + ); + + yield new Showcase( + id: 'cacheable', + group: 'Advices', + title: 'Caching', + summary: 'An Around advice caches the result of #[Cacheable] methods: the slow method runs only once.', + aspect: CachingAspect::class, + scenario: static function (): CacheableDemo { + $example = new CacheableDemo(); + $result = $example->getReport('Test'); // First call takes 0.1 second + echo 'Result is: ', $result, PHP_EOL; + + $result = $example->getReport('Test1'); // This call is cached and the result is still 'Test' + echo 'Result is: ', $result, PHP_EOL; + + return $example; + }, + ); + + yield new Showcase( + id: 'after-throwing', + group: 'Advices', + title: 'Reporting exceptions', + summary: 'An AfterThrowing advice reports the failures of a payment gateway, the caller still handles the exception.', + aspect: ErrorMonitoringAspect::class, + scenario: static function (): PaymentDemo { + $example = new PaymentDemo(); + echo $example->charge('4111-1111', 50), PHP_EOL; // No exception, the advice is not called + try { + $example->charge('4111-1111', 0); // The advice reports the exception, then it is rethrown + } catch (InvalidArgumentException $exception) { + echo 'Caller handled the exception: ', $exception->getMessage(), PHP_EOL; + } + + return $example; + }, + ); + + yield new Showcase( + id: 'private-methods', + group: 'Interceptors', + title: 'Private and protected methods', + summary: 'Every internal step of an order processor is intercepted, although only its public method is called.', + aspect: PrivateMethodAspect::class, + scenario: static function (): OrderProcessorDemo { + $example = new OrderProcessorDemo(); + $total = $example->process(['book' => 42.5, 'lamp' => 80.0]); // Only the public method is called here + echo 'Order total: ', $total, PHP_EOL; + + return $example; + }, + ); + + yield new Showcase( + id: 'property-interceptor', + group: 'Interceptors', + title: 'Property access', + summary: 'An access() pointcut intercepts reads and writes of public, protected and private properties.', + aspect: PropertyInterceptorAspect::class, + scenario: static function (): PropertyDemo { + $example = new PropertyDemo(); + echo $example->publicProperty, PHP_EOL; // Read public property + $example->publicProperty = 987; // Write public property + $example->showProtected(); + $example->setProtected(987); + + return $example; + }, + ); + + yield new Showcase( + id: 'property-hooks', + group: 'Interceptors', + title: 'Property hooks', + summary: 'Plain and asymmetric-visibility properties of PHP 8.4 are intercepted, a property with its own hook is left alone.', + aspect: PropertyHooksAspect::class, + scenario: static function (): ProductDemo { + $example = new ProductDemo('go-aop-4'); // The own "set" hook of $sku uppercases the value + $example->price = 19.99; + $example->restock(5); // Reads and writes the asymmetric $stock property + echo "Product {$example->sku} costs {$example->price}, {$example->stock} in stock", PHP_EOL; + + return $example; + }, + ); + + yield new Showcase( + id: 'enum-interceptor', + group: 'Interceptors', + title: 'Enum methods', + summary: 'Instance and static methods of a backed enum are intercepted just like the methods of a class.', + aspect: EnumInterceptorAspect::class, + scenario: static function (): OrderStatus { + $example = OrderStatus::Pending; + foreach (OrderStatus::cases() as $status) { + echo $status->label(), PHP_EOL; + } + $next = OrderStatus::next($example); + echo 'Next status after pending: ', $next->name, PHP_EOL; + + return $example; + }, + ); + + yield new Showcase( + id: 'function-interceptor', + group: 'Interceptors', + title: 'System functions', + summary: 'Calls of array_*() and file_get_contents() inside a namespace are intercepted, an advice even replaces the result.', + aspect: FunctionInterceptorAspect::class, + scenario: static function (): FunctionDemo { + $example = new FunctionDemo(); + echo 'Unique values: ', json_encode($example->testArrayFunctions(['test' => 1, 'code' => 2, 'more' => 1])), PHP_EOL; + $example->testFileContent(); + + return $example; + }, + ); + + yield new Showcase( + id: 'dynamic-interceptor', + group: 'Interceptors', + title: 'Magic methods', + summary: 'Calls going through __call() and __callStatic() are intercepted with the name of the called method.', + aspect: DynamicMethodsAspect::class, + scenario: static function (): DynamicMethodsDemo { + $example = new DynamicMethodsDemo(); + $example->saveById(123); // Intercept magic dynamic method + $example->load(456); // The advice filters out this method by name + DynamicMethodsDemo::find(['id' => 124]); // Intercept magic static method + + return $example; + }, + ); + + yield new Showcase( + id: 'fluent-interface', + group: 'Advanced', + title: 'Fluent interface', + summary: 'An Around advice makes the setters return $this for every class implementing the FluentInterface marker.', + aspect: FluentInterfaceAspect::class, + scenario: static function (): UserFluentDemo { + $example = new UserFluentDemo(); // Original class doesn't provide fluent interface for us + if ($example instanceof Proxy) { // This check keeps the example working when AOP is disabled + $example + ->setName('John') + ->setSurname('Doe') + ->setPassword('root'); + } else { + echo 'Fluent interface is not available without AOP', PHP_EOL; + } + + return $example; + }, + ); + + yield new Showcase( + id: 'human-advices', + group: 'Advanced', + title: 'Human life advices', + summary: 'Before and After advices take care of a healthy day: wash up before eating, clean the teeth after it.', + aspect: HealthyLiveAspect::class, + scenario: static function (): HumanDemo { + $example = new HumanDemo(); + echo "Want to eat something, let's have a breakfast!", PHP_EOL; + $example->eat(); + echo 'I should work to earn some money', PHP_EOL; + $example->work(); + echo 'It was a nice day, go to bed', PHP_EOL; + $example->sleep(); + + return $example; + }, + ); + + yield new Showcase( + id: 'dynamic-traits', + group: 'Advanced', + title: 'Dynamic traits and interfaces', + summary: 'An introduction adds the Stringable interface and a trait implementing it to an existing class.', + aspect: IntroductionAspect::class, + scenario: static function (): IntroductionDemo { + $example = new IntroductionDemo(); // Original class doesn't implement Stringable + $example->testStringable(); + + return $example; + }, + ); + } +} diff --git a/demos/Demo/Showcase/ShowcaseResult.php b/demos/Demo/Showcase/ShowcaseResult.php new file mode 100644 index 000000000..57c9e853b --- /dev/null +++ b/demos/Demo/Showcase/ShowcaseResult.php @@ -0,0 +1,45 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Demo\Showcase; + +/** + * Everything the page shows about an executed showcase + */ +final readonly class ShowcaseResult +{ + /** + * @param string $output Text printed by the scenario and the advices + * @param float $elapsedMs Execution time of the scenario + * @param SourceCode $usage Code of the scenario itself + * @param SourceCode $aspect Source code of the aspect + * @param SourceCode|null $subject Source code of the class the scenario works with + */ + public function __construct( + public string $output, + public float $elapsedMs, + public SourceCode $usage, + public SourceCode $aspect, + public ?SourceCode $subject, + ) {} + + /** + * @return array Source code panels indexed by their label + */ + public function sources(): array + { + return array_filter( + ['Usage' => $this->usage, 'Aspect' => $this->aspect, 'Class' => $this->subject], + static fn(?SourceCode $source): bool => $source !== null, + ); + } +} diff --git a/demos/Demo/Showcase/SourceCode.php b/demos/Demo/Showcase/SourceCode.php new file mode 100644 index 000000000..0581ae3c8 --- /dev/null +++ b/demos/Demo/Showcase/SourceCode.php @@ -0,0 +1,89 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Demo\Showcase; + +use Closure; +use Go\Aop\Proxy; +use Go\Instrument\Transformer\MagicConstantTransformer; +use ReflectionClass; +use ReflectionFunction; + +/** + * Piece of PHP source code shown on the page + */ +final readonly class SourceCode +{ + /** + * Root directory of the demo, file names are shown relative to it + */ + private const string DEMO_ROOT = __DIR__ . '/../../'; + + public function __construct( + public string $fileName, + public string $code, + ) {} + + /** + * Source code of the whole file that declares the class + * + * A woven class is loaded from the cache directory, its original file is shown instead + * + * @param ReflectionClass<*> $class + */ + public static function ofClass(ReflectionClass $class): self + { + $fileName = (string) $class->getFileName(); + if ($class->implementsInterface(Proxy::class)) { + $fileName = MagicConstantTransformer::resolveFileName($fileName); + } + + return new self(self::relative($fileName), (string) file_get_contents($fileName)); + } + + /** + * Body of the closure, without its signature and the closing brace + */ + public static function ofClosure(Closure $closure): self + { + $function = new ReflectionFunction($closure); + $fileName = (string) $function->getFileName(); + $lines = file($fileName, FILE_IGNORE_NEW_LINES); + $start = (int) $function->getStartLine(); + $body = array_slice($lines === false ? [] : $lines, $start, (int) $function->getEndLine() - $start - 1); + + return new self(self::relative($fileName), self::dedent($body)); + } + + private static function relative(string $fileName): string + { + $root = (string) realpath(self::DEMO_ROOT) . DIRECTORY_SEPARATOR; + + return str_starts_with($fileName, $root) ? substr($fileName, strlen($root)) : basename($fileName); + } + + /** + * @param list $lines + */ + private static function dedent(array $lines): string + { + $indents = []; + foreach ($lines as $line) { + if (trim($line) !== '') { + $indents[] = strlen($line) - strlen(ltrim($line)); + } + } + $indent = $indents === [] ? 0 : min($indents); + + return implode(PHP_EOL, array_map(static fn(string $line): string => substr($line, $indent), $lines)); + } +} diff --git a/demos/assets/demo.css b/demos/assets/demo.css new file mode 100644 index 000000000..bade8a864 --- /dev/null +++ b/demos/assets/demo.css @@ -0,0 +1,992 @@ +/* + * Go! AOP demo — styles + * Dark theme by default, light theme follows the system preference. + */ + +:root { + --bg: #070b18; + --bg-elevated: rgba(17, 24, 46, 0.72); + --bg-sunken: rgba(5, 8, 20, 0.78); + --border: rgba(148, 163, 255, 0.14); + --border-strong: rgba(148, 163, 255, 0.28); + --text: #e6e9f5; + --text-muted: #94a0c2; + --text-faint: #66708f; + --accent: #8b6cff; + --accent-2: #22d3ee; + --accent-3: #f472b6; + --gradient: linear-gradient(120deg, var(--accent) 0%, var(--accent-2) 100%); + --on: #34d399; + --off: #fb7185; + --shadow: 0 20px 50px -20px rgba(0, 0, 0, 0.6); + --glow: 0 0 0 1px rgba(139, 108, 255, 0.35), 0 12px 40px -12px rgba(139, 108, 255, 0.55); + --radius: 16px; + --radius-small: 10px; + --topbar-height: 64px; + --font: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + --mono: ui-monospace, "JetBrains Mono", "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace; + + /* Code highlighting (highlight.js classes) */ + --code-keyword: #c792ea; + --code-string: #a5e075; + --code-number: #f78c6c; + --code-comment: #6b7599; + --code-variable: #82aaff; + --code-title: #ffcb6b; + --code-meta: #89ddff; +} + +@media (prefers-color-scheme: light) { + :root { + --bg: #f4f6fc; + --bg-elevated: rgba(255, 255, 255, 0.78); + --bg-sunken: rgba(237, 240, 250, 0.9); + --border: rgba(60, 72, 140, 0.13); + --border-strong: rgba(60, 72, 140, 0.26); + --text: #161b33; + --text-muted: #4f587a; + --text-faint: #8a92b0; + --accent: #6d4aff; + --accent-2: #0891b2; + --on: #059669; + --off: #e11d48; + --shadow: 0 20px 45px -25px rgba(30, 40, 90, 0.35); + --glow: 0 0 0 1px rgba(109, 74, 255, 0.3), 0 12px 32px -14px rgba(109, 74, 255, 0.45); + + --code-keyword: #8b3fd9; + --code-string: #2f7d32; + --code-number: #c2410c; + --code-comment: #8a92b0; + --code-variable: #2453c9; + --code-title: #9a5b00; + --code-meta: #0e7490; + } +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + min-height: 100vh; + background: var(--bg); + color: var(--text); + font: 15px/1.6 var(--font); + -webkit-font-smoothing: antialiased; +} + +a { + color: inherit; +} + +svg { + width: 20px; + height: 20px; + fill: none; + stroke: currentColor; + stroke-width: 1.8; + stroke-linecap: round; + stroke-linejoin: round; +} + +:focus-visible { + outline: 2px solid var(--accent-2); + outline-offset: 3px; + border-radius: 6px; +} + +/* Animated gradient background */ + +.backdrop { + position: fixed; + inset: 0; + z-index: -1; + overflow: hidden; + pointer-events: none; +} + +.backdrop::before, +.backdrop::after { + content: ""; + position: absolute; + width: 60vmax; + height: 60vmax; + border-radius: 50%; + filter: blur(90px); + opacity: 0.32; + animation: drift 26s ease-in-out infinite alternate; +} + +.backdrop::before { + top: -25vmax; + left: -15vmax; + background: radial-gradient(circle, var(--accent) 0%, transparent 65%); +} + +.backdrop::after { + right: -20vmax; + bottom: -30vmax; + background: radial-gradient(circle, var(--accent-2) 0%, transparent 65%); + animation-delay: -13s; +} + +@keyframes drift { + from { transform: translate3d(0, 0, 0) scale(1); } + to { transform: translate3d(8vmax, 6vmax, 0) scale(1.15); } +} + +/* Top bar */ + +.topbar { + position: sticky; + top: 0; + z-index: 30; + display: flex; + align-items: center; + gap: 16px; + height: var(--topbar-height); + padding: 0 24px; + background: var(--bg-elevated); + border-bottom: 1px solid var(--border); + backdrop-filter: blur(18px) saturate(160%); + -webkit-backdrop-filter: blur(18px) saturate(160%); +} + +.brand { + display: flex; + align-items: center; + gap: 10px; + text-decoration: none; + font-weight: 700; + letter-spacing: -0.01em; +} + +.brand-mark { + display: grid; + place-items: center; + width: 36px; + height: 36px; + border-radius: 10px; + background: var(--gradient); + color: #fff; + font-size: 14px; + box-shadow: var(--glow); +} + +.brand-name { + font-size: 17px; +} + +.brand-tag { + margin-left: 4px; + padding: 2px 8px; + border: 1px solid var(--border-strong); + border-radius: 999px; + color: var(--text-muted); + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + vertical-align: 2px; +} + +.topbar-links { + display: flex; + gap: 4px; + margin-left: auto; +} + +.topbar-links a { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 12px; + border-radius: var(--radius-small); + color: var(--text-muted); + text-decoration: none; + font-weight: 500; + transition: color 0.2s, background-color 0.2s; +} + +.topbar-links a:hover { + color: var(--text); + background: rgba(139, 108, 255, 0.12); +} + +.icon-button { + display: none; + place-items: center; + width: 40px; + height: 40px; + padding: 0; + border: 1px solid var(--border); + border-radius: var(--radius-small); + background: transparent; + color: var(--text); + cursor: pointer; +} + +/* AOP switch */ + +.aop-switch { + margin: 0; +} + +.aop-switch button { + display: flex; + align-items: center; + gap: 10px; + padding: 6px 12px 6px 14px; + border: 1px solid var(--border-strong); + border-radius: 999px; + background: var(--bg-sunken); + color: var(--text); + font: inherit; + font-weight: 600; + cursor: pointer; + transition: border-color 0.25s, box-shadow 0.25s, transform 0.15s; +} + +.aop-switch button:hover { + border-color: var(--accent); + box-shadow: var(--glow); +} + +.aop-switch button:active { + transform: scale(0.97); +} + +.aop-switch-label { + font-size: 12px; + letter-spacing: 0.1em; + color: var(--text-muted); +} + +.aop-switch-track { + position: relative; + width: 46px; + height: 26px; + border-radius: 999px; + background: color-mix(in srgb, var(--off) 30%, transparent); + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--off) 55%, transparent); + transition: background-color 0.3s, box-shadow 0.3s; +} + +.aop-switch-thumb { + position: absolute; + top: 3px; + left: 3px; + width: 20px; + height: 20px; + border-radius: 50%; + background: #fff; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35); + transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.aop-switch-state { + min-width: 26px; + color: var(--off); + font-size: 13px; +} + +.aop-switch [aria-checked="true"] .aop-switch-track { + background: color-mix(in srgb, var(--on) 75%, transparent); + box-shadow: inset 0 0 0 1px var(--on), 0 0 18px -2px var(--on); +} + +.aop-switch [aria-checked="true"] .aop-switch-thumb { + transform: translateX(20px); +} + +.aop-switch [aria-checked="true"] .aop-switch-state { + color: var(--on); +} + +.aop-switch.is-pending .aop-switch-thumb { + animation: pulse 0.8s ease-in-out infinite; +} + +@keyframes pulse { + 50% { box-shadow: 0 0 0 6px rgba(255, 255, 255, 0.15); } +} + +/* Layout */ + +.layout { + display: grid; + grid-template-columns: 280px minmax(0, 1fr); + max-width: 1440px; + margin: 0 auto; +} + +.sidebar { + position: sticky; + top: var(--topbar-height); + height: calc(100vh - var(--topbar-height)); + overflow-y: auto; + padding: 28px 16px 40px 24px; + scrollbar-width: thin; +} + +.nav-group { + margin: 22px 0 8px 12px; + color: var(--text-faint); + font-size: 11px; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.nav-group:first-child { + margin-top: 0; +} + +.nav-list, +.resources ul { + margin: 0; + padding: 0; + list-style: none; +} + +.nav-link { + position: relative; + display: block; + padding: 8px 12px; + border-radius: var(--radius-small); + color: var(--text-muted); + text-decoration: none; + font-weight: 500; + transition: color 0.2s, background-color 0.2s, transform 0.2s; +} + +.nav-link::before { + content: ""; + position: absolute; + top: 8px; + bottom: 8px; + left: 0; + width: 3px; + border-radius: 3px; + background: var(--gradient); + transform: scaleY(0); + transition: transform 0.25s ease; +} + +.nav-link:hover { + color: var(--text); + background: rgba(139, 108, 255, 0.08); + transform: translateX(2px); +} + +.nav-link.is-active { + color: var(--text); + background: linear-gradient(90deg, rgba(139, 108, 255, 0.2), rgba(34, 211, 238, 0.06)); +} + +.nav-link.is-active::before { + transform: scaleY(1); +} + +.resources { + margin-top: 28px; + padding-top: 18px; + border-top: 1px solid var(--border); +} + +.resources details { + margin-bottom: 4px; +} + +.resources summary { + padding: 8px 12px; + border-radius: var(--radius-small); + color: var(--text-muted); + font-weight: 600; + cursor: pointer; + list-style: none; + transition: background-color 0.2s, color 0.2s; +} + +.resources summary::-webkit-details-marker { + display: none; +} + +.resources summary::after { + content: "+"; + float: right; + color: var(--text-faint); + transition: transform 0.25s; +} + +.resources details[open] summary::after { + transform: rotate(45deg); +} + +.resources summary:hover { + color: var(--text); + background: rgba(139, 108, 255, 0.08); +} + +.resources li a { + display: block; + padding: 6px 12px 6px 22px; + color: var(--text-muted); + font-size: 13.5px; + text-decoration: none; + transition: color 0.2s; +} + +.resources li a:hover { + color: var(--accent-2); +} + +.content { + min-width: 0; + padding: 40px 40px 32px; + animation: rise 0.5s ease both; +} + +@keyframes rise { + from { opacity: 0; transform: translateY(12px); } + to { opacity: 1; transform: none; } +} + +/* Typography */ + +h1, +h2, +h3 { + margin: 0; + letter-spacing: -0.02em; + line-height: 1.2; +} + +.eyebrow { + margin: 0 0 10px; + color: var(--accent-2); + font-size: 12px; + font-weight: 700; + letter-spacing: 0.14em; + text-transform: uppercase; +} + +.lead { + max-width: 70ch; + margin: 14px 0 0; + color: var(--text-muted); + font-size: 17px; +} + +.gradient-text { + background: var(--gradient); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} + +.muted { + color: var(--text-faint); + font-style: italic; +} + +/* Landing */ + +.hero { + padding: 24px 0 40px; +} + +.hero h1 { + max-width: 18ch; + font-size: clamp(34px, 5vw, 56px); + font-weight: 800; +} + +.hero-actions { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 28px; +} + +.button { + display: inline-flex; + align-items: center; + padding: 11px 20px; + border-radius: 12px; + font-weight: 600; + text-decoration: none; + transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s; +} + +.button:hover { + transform: translateY(-2px); +} + +.button-primary { + background: var(--gradient); + color: #fff; + box-shadow: var(--glow); +} + +.button-ghost { + border: 1px solid var(--border-strong); + background: var(--bg-elevated); +} + +.button-ghost:hover { + border-color: var(--accent); +} + +.card-group { + margin-top: 32px; +} + +.section-title { + margin-bottom: 14px; + color: var(--text-faint); + font-size: 12px; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.card-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 16px; +} + +.card { + position: relative; + display: flex; + flex-direction: column; + gap: 8px; + padding: 22px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--bg-elevated); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + text-decoration: none; + box-shadow: var(--shadow); + animation: rise 0.5s ease both; + animation-delay: calc(var(--i, 0) * 60ms); + transition: transform 0.25s ease, border-color 0.25s, box-shadow 0.25s; +} + +.card::before { + content: ""; + position: absolute; + inset: -1px; + border-radius: inherit; + padding: 1px; + background: var(--gradient); + -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + opacity: 0; + transition: opacity 0.25s; +} + +.card:hover { + transform: translateY(-4px); + box-shadow: var(--glow); +} + +.card:hover::before { + opacity: 1; +} + +.card h3 { + font-size: 17px; +} + +.card p { + flex: 1; + margin: 0; + color: var(--text-muted); + font-size: 14px; +} + +.card-link { + color: var(--accent-2); + font-size: 13px; + font-weight: 600; +} + +.card-link span { + display: inline-block; + transition: transform 0.2s; +} + +.card:hover .card-link span { + transform: translateX(4px); +} + +/* Showcase */ + +.showcase-header { + margin-bottom: 24px; +} + +.showcase-header h1 { + font-size: clamp(28px, 4vw, 40px); + font-weight: 800; +} + +.chips { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin: 18px 0 0; + padding: 0; + list-style: none; +} + +.chip { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 5px 12px; + border: 1px solid var(--border); + border-radius: 999px; + background: var(--bg-elevated); + font-family: var(--mono); + font-size: 12.5px; +} + +.chip-key { + color: var(--text-faint); + font-family: var(--font); + font-size: 11px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.chip-on { + border-color: color-mix(in srgb, var(--on) 50%, transparent); + color: var(--on); +} + +.chip-off { + border-color: color-mix(in srgb, var(--off) 50%, transparent); + color: var(--off); +} + +.callout { + margin: 0 0 20px; + padding: 14px 18px; + border: 1px solid color-mix(in srgb, var(--off) 40%, transparent); + border-left-width: 4px; + border-radius: var(--radius-small); + background: color-mix(in srgb, var(--off) 9%, transparent); + color: var(--text); +} + +.panel { + margin-bottom: 24px; + overflow: hidden; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--bg-elevated); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + box-shadow: var(--shadow); +} + +.panel-header { + display: flex; + align-items: center; + gap: 14px; + padding: 12px 16px; + border-bottom: 1px solid var(--border); +} + +.panel-header h2 { + color: var(--text-muted); + font-size: 13px; + font-weight: 600; +} + +.dots { + display: flex; + gap: 6px; +} + +.dots i { + width: 11px; + height: 11px; + border-radius: 50%; + background: #ff5f57; +} + +.dots i:nth-child(2) { + background: #febc2e; +} + +.dots i:nth-child(3) { + background: #28c840; +} + +.terminal-body, +.code { + margin: 0; + overflow: auto; + background: var(--bg-sunken); + font: 13px/1.65 var(--mono); + tab-size: 4; +} + +.terminal-body { + max-height: 420px; + padding: 18px 20px; + color: var(--text); + white-space: pre-wrap; + word-break: break-word; +} + +.code { + max-height: 640px; + padding: 18px 20px; +} + +.copy-button { + margin-left: auto; + padding: 4px 12px; + border: 1px solid var(--border-strong); + border-radius: 8px; + background: transparent; + color: var(--text-muted); + font: 600 12px var(--font); + cursor: pointer; + transition: color 0.2s, border-color 0.2s, background-color 0.2s; +} + +.copy-button:hover { + border-color: var(--accent); + color: var(--text); +} + +.copy-button.is-done { + border-color: var(--on); + color: var(--on); +} + +/* Source tabs */ + +.tabs { + display: flex; + gap: 4px; + padding: 8px 8px 0; + border-bottom: 1px solid var(--border); +} + +.no-js .tabs { + display: none; +} + +.tabs [role="tab"] { + position: relative; + padding: 10px 16px 12px; + border: 0; + background: transparent; + color: var(--text-muted); + font: 600 14px var(--font); + cursor: pointer; + transition: color 0.2s; +} + +.tabs [role="tab"]::after { + content: ""; + position: absolute; + right: 12px; + bottom: -1px; + left: 12px; + height: 2px; + border-radius: 2px; + background: var(--gradient); + transform: scaleX(0); + transition: transform 0.25s ease; +} + +.tabs [role="tab"]:hover, +.tabs [aria-selected="true"] { + color: var(--text); +} + +.tabs [aria-selected="true"]::after { + transform: scaleX(1); +} + +.tab-panel[hidden] { + display: none; +} + +.tab-panel:not([hidden]) { + animation: rise 0.3s ease both; +} + +.file-bar { + display: flex; + align-items: center; + padding: 10px 16px; + border-bottom: 1px solid var(--border); +} + +.file-name { + color: var(--text-faint); + font: 12.5px var(--mono); +} + +/* highlight.js token colours */ + +.hljs-keyword, +.hljs-built_in, +.hljs-literal, +.hljs-selector-tag { + color: var(--code-keyword); +} + +.hljs-string, +.hljs-subst { + color: var(--code-string); +} + +.hljs-number { + color: var(--code-number); +} + +.hljs-comment, +.hljs-doctag, +.hljs-quote { + color: var(--code-comment); + font-style: italic; +} + +.hljs-doctag { + font-style: normal; +} + +.hljs-variable, +.hljs-params, +.hljs-attr { + color: var(--code-variable); +} + +.hljs-title, +.hljs-title.class_, +.hljs-title.function_, +.hljs-type { + color: var(--code-title); +} + +.hljs-meta, +.hljs-operator, +.hljs-punctuation { + color: var(--code-meta); +} + +/* Footer */ + +.footer { + margin-top: 48px; + padding-top: 20px; + border-top: 1px solid var(--border); + color: var(--text-faint); + font-size: 13px; +} + +.footer a { + color: var(--text-muted); +} + +/* Mobile: the sidebar becomes a drawer */ + +.scrim { + display: none; +} + +@media (max-width: 960px) { + .icon-button { + display: grid; + } + + .topbar { + gap: 10px; + padding: 0 16px; + } + + .topbar-links span, + .brand-tag { + display: none; + } + + .layout { + grid-template-columns: minmax(0, 1fr); + } + + .sidebar { + position: fixed; + top: var(--topbar-height); + bottom: 0; + left: 0; + z-index: 25; + width: min(300px, 85vw); + height: auto; + background: var(--bg-elevated); + border-right: 1px solid var(--border); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + transform: translateX(-102%); + transition: transform 0.3s ease; + } + + .scrim { + position: fixed; + inset: var(--topbar-height) 0 0; + z-index: 20; + display: block; + background: rgba(0, 0, 0, 0.45); + opacity: 0; + pointer-events: none; + transition: opacity 0.3s; + } + + .is-menu-open .sidebar { + transform: none; + } + + .is-menu-open .scrim { + opacity: 1; + pointer-events: auto; + } + + .content { + padding: 28px 16px 24px; + } +} + +@media (max-width: 520px) { + .aop-switch-label { + display: none; + } +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation: none !important; + transition: none !important; + scroll-behavior: auto !important; + } +} diff --git a/demos/assets/demo.js b/demos/assets/demo.js new file mode 100644 index 000000000..be8436f97 --- /dev/null +++ b/demos/assets/demo.js @@ -0,0 +1,115 @@ +/* + * Go! AOP demo — behaviour of the page + * The page works without this script: the AOP switch is a plain form and all sources are shown. + */ +(function () { + 'use strict'; + + document.documentElement.classList.replace('no-js', 'js'); + + // AOP switch: flips the knob first, then submits the form that stores the new state + document.querySelectorAll('[data-aop-switch]').forEach(function (form) { + var initialState = form.querySelector('[role="switch"]').getAttribute('aria-checked'); + + // Back/forward cache restores the page as it was left: with the knob already flipped + window.addEventListener('pageshow', function () { + form.classList.remove('is-pending'); + form.querySelector('[role="switch"]').setAttribute('aria-checked', initialState); + }); + + form.addEventListener('submit', function (event) { + if (form.classList.contains('is-pending')) { + return; + } + event.preventDefault(); + var button = form.querySelector('[role="switch"]'); + button.setAttribute('aria-checked', String(button.getAttribute('aria-checked') !== 'true')); + form.classList.add('is-pending'); + window.setTimeout(function () { + form.submit(); + }, 220); + }); + }); + + // Source code tabs + document.querySelectorAll('[data-tabs]').forEach(function (container) { + var tabs = Array.prototype.slice.call(container.querySelectorAll('[role="tab"]')); + + function select(tab, focus) { + tabs.forEach(function (other) { + var isSelected = other === tab; + other.setAttribute('aria-selected', String(isSelected)); + other.tabIndex = isSelected ? 0 : -1; + document.getElementById(other.getAttribute('aria-controls')).hidden = !isSelected; + }); + if (focus) { + tab.focus(); + } + } + + tabs.forEach(function (tab, index) { + tab.addEventListener('click', function () { + select(tab, false); + }); + tab.addEventListener('keydown', function (event) { + var offset = { ArrowRight: 1, ArrowLeft: -1 }[event.key]; + if (offset) { + event.preventDefault(); + select(tabs[(index + offset + tabs.length) % tabs.length], true); + } + }); + }); + if (tabs.length > 0) { + select(tabs[0], false); + } + }); + + // Copy buttons + document.querySelectorAll('[data-copy]').forEach(function (button) { + button.addEventListener('click', function () { + var source = document.querySelector(button.getAttribute('data-copy')); + if (!source || !navigator.clipboard) { + return; + } + navigator.clipboard.writeText(source.textContent).then(function () { + button.textContent = 'Copied'; + button.classList.add('is-done'); + window.setTimeout(function () { + button.textContent = 'Copy'; + button.classList.remove('is-done'); + }, 1500); + }); + }); + }); + + // Navigation drawer on small screens + var menuButton = document.querySelector('[data-menu-toggle]'); + + function toggleMenu(isOpen) { + document.body.classList.toggle('is-menu-open', isOpen); + if (menuButton) { + menuButton.setAttribute('aria-expanded', String(isOpen)); + } + } + + if (menuButton) { + menuButton.addEventListener('click', function () { + toggleMenu(!document.body.classList.contains('is-menu-open')); + }); + } + document.querySelectorAll('[data-menu-close]').forEach(function (element) { + element.addEventListener('click', function () { + toggleMenu(false); + }); + }); + document.addEventListener('keydown', function (event) { + if (event.key === 'Escape') { + toggleMenu(false); + } + }); + + // Syntax highlighting, the code stays readable as plain text when the library is not available + if (window.hljs) { + window.hljs.highlightAll(); + } +})(); diff --git a/demos/index.php b/demos/index.php index eab91f5df..90574842a 100644 --- a/demos/index.php +++ b/demos/index.php @@ -1,6 +1,6 @@ getReport('Test'); // First call will take 0.1 second - echo "Result is: ", $result, PHP_EOL; - - $result = $example->getReport('Test1'); // This call is cached and result should be 'Test' - echo "Result is: ", $result, PHP_EOL; - break; - - case 'loggable': - $aspectName = 'Demo\Aspect\LoggingAspect'; - - $example = new LoggingDemo(); - $example->execute('LoggingTask'); // Logging for dynamic methods - LoggingDemo::runByName('StaticTask'); // Logging for static methods - break; - - case 'after-throwing': - $aspectName = 'Demo\Aspect\ErrorMonitoringAspect'; - - $example = new PaymentDemo(); - echo $example->charge('4111-1111', 50), PHP_EOL; // No exception, the advice is not called - try { - $example->charge('4111-1111', 0); // The advice reports the exception, then it is rethrown - } catch (InvalidArgumentException $exception) { - echo 'Caller handled the exception: ', $exception->getMessage(), PHP_EOL; - } - break; - - case 'private-methods': - $aspectName = 'Demo\Aspect\PrivateMethodAspect'; - - $example = new OrderProcessorDemo(); - $total = $example->process(['book' => 42.5, 'lamp' => 80.0]); // Only the public method is called here - echo 'Order total: ', $total, PHP_EOL; - break; - - case 'property-interceptor': - $aspectName = 'Demo\Aspect\PropertyInterceptorAspect'; - - $example = new PropertyDemo(); - echo $example->publicProperty, PHP_EOL; // Read public property - $example->publicProperty = 987; // Write public property - $example->showProtected(); - $example->setProtected(987); - break; - - case 'property-hooks': - $aspectName = 'Demo\Aspect\PropertyHooksAspect'; - - $example = new ProductDemo('go-aop-4'); // The own "set" hook of $sku uppercases the value - $example->price = 19.99; - $example->restock(5); // Reads and writes the asymmetric $stock property - echo "Product {$example->sku} costs {$example->price}, {$example->stock} in stock", PHP_EOL; - break; - - case 'enum-interceptor': - $aspectName = 'Demo\Aspect\EnumInterceptorAspect'; - - $example = OrderStatus::Pending; - foreach (OrderStatus::cases() as $status) { - echo $status->label(), PHP_EOL; - } - $next = OrderStatus::next($example); - echo 'Next status after pending: ', $next->name, PHP_EOL; - break; - - case 'dynamic-interceptor': - $aspectName = 'Demo\Aspect\DynamicMethodsAspect'; - - $example = new DynamicMethodsDemo(); - $example->saveById(123); // intercept magic dynamic method - $example->load(456); // the advice filters out this method by name - DynamicMethodsDemo::find(['id' => 124]); // intercept magic static method - break; - - case 'function-interceptor': - $aspectName = 'Demo\Aspect\FunctionInterceptorAspect'; - - $example = new FunctionDemo(); - echo 'Unique values: ', json_encode($example->testArrayFunctions(['test' => 1, 'code' => 2, 'more' => 1])), PHP_EOL; - $example->testFileContent(); - break; - - case 'fluent-interface': - $aspectName = 'Demo\Aspect\FluentInterfaceAspect'; - - $example = new UserFluentDemo(); // Original class doesn't provide fluent interface for us - if ($example instanceof Proxy) { // This check is to prevent fatal errors when AOP is disabled - $example - ->setName('John') - ->setSurname('Doe') - ->setPassword('root'); - } else { - echo "Fluent interface is not available without AOP", PHP_EOL; - } - break; - - case 'human-advices': - $aspectName = 'Demo\Aspect\HealthyLiveAspect'; +/* + * Front controller of the demo: runs the selected showcase and renders the page from templates/page.php, + * the styles and scripts of the page live in assets/. + */ - $example = new HumanDemo(); - echo "Want to eat something, let's have a breakfast!", PHP_EOL; - $example->eat(); - echo "I should work to earn some money", PHP_EOL; - $example->work(); - echo "It was a nice day, go to bed", PHP_EOL; - $example->sleep(); - break; +use Demo\Showcase\ShowcaseCatalog; - case 'dynamic-traits': - $aspectName = 'Demo\Aspect\IntroductionAspect'; +const AOP_COOKIE = 'aop'; - $example = new IntroductionDemo(); // Original class doesn't implement Stringable - $example->testStringable(); - break; +// The AOP switch posts the new state: it is remembered in a cookie, then the page is reloaded with GET +if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST' && isset($_POST['aop'])) { + $scriptName = is_string($_SERVER['SCRIPT_NAME'] ?? null) ? $_SERVER['SCRIPT_NAME'] : '/'; + $requestUri = is_string($_SERVER['REQUEST_URI'] ?? null) ? $_SERVER['REQUEST_URI'] : './'; + setcookie(AOP_COOKIE, $_POST['aop'] === 'off' ? 'off' : 'on', [ + 'expires' => time() + 30 * 86400, + 'path' => rtrim(dirname($scriptName), '/') . '/', + 'httponly' => true, + 'samesite' => 'Lax', + ]); + header('Location: ' . $requestUri, true, 303); - default: + return; } -$output = (string) ob_get_clean(); - -// Woven classes are loaded from the cache: the page shows their original source files -$sourceFile = static fn(string|false $path): string => MagicConstantTransformer::resolveFileName((string) $path); -?> - - - - - Go! AOP Demo - - - - -
- -
- -
- -

Welcome

-

This demo shows examples of AOP usage.

- -
-
-

- Choose one of the examples from the navigation menu. - You can also run this code with Xdebug. -

-
-
-
- - -
- Source code of the aspect -
- getFileName())); ?> -
-
- - - -
- Source code of the class -
- getFileName())); ?> -
-
- +$isAopEnabled = ($_COOKIE[AOP_COOKIE] ?? 'on') !== 'off'; +require __DIR__ . ($isAopEnabled ? '/autoload_aspect.php' : '/../vendor/autoload.php'); -
+$catalog = new ShowcaseCatalog(); +$showcase = $catalog->find(is_string($_GET['showcase'] ?? null) ? $_GET['showcase'] : ''); +$result = $showcase?->run(); - - - - +require __DIR__ . '/templates/page.php'; diff --git a/demos/templates/page.php b/demos/templates/page.php new file mode 100644 index 000000000..f36c36017 --- /dev/null +++ b/demos/templates/page.php @@ -0,0 +1,205 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +/* + * Page of the demo, rendered by index.php. Presentation only: styles are in assets/demo.css, + * behaviour (AOP switch, source tabs, mobile navigation, code highlighting) is in assets/demo.js. + */ + +use Demo\Showcase\Showcase; +use Demo\Showcase\ShowcaseCatalog; +use Demo\Showcase\ShowcaseResult; + +/** @var ShowcaseCatalog $catalog */ +/** @var Showcase|null $showcase */ +/** @var ShowcaseResult|null $result */ +/** @var bool $isAopEnabled */ +?> + + + + + + + <?= $showcase !== null ? htmlspecialchars($showcase->title) . ' · ' : '' ?>Go! AOP Demo + + + + + + + + + +
+ + + + Go! + AOP demo + + + + +
+ + +
+
+ +
+ + + +
+ +
+ + +
+

Aspect-oriented programming for PHP

+

Cross-cutting concerns, woven in at load time.

+

+ Each example runs plain PHP classes. The aspects add logging, caching, error reporting and more + without touching their code. Flip the AOP switch to run the same code without them. +

+ +
+ + groups() as $group => $items): ?> +
+

+ +
+ + + + +
+

group) ?>

+

title) ?>

+

summary) ?>

+
    +
  • AspectaspectName()) ?>
  • +
  • AOP
  • +
  • TimeelapsedMs, 2) ?> ms
  • +
+
+ + +

+ AOP is switched off: the example runs exactly as written, without its aspect. + Compare the output with the woven version by switching it on again. +

+ + +
+
+ +

Output

+ +
+
output !== '' ? htmlspecialchars($result->output) : 'The example printed nothing.' ?>
+
+ +
+
+ sources()) as $index => $label): ?> + + +
+ sources()) as $index => $source): ?> +
+
+ fileName) ?> + +
+
code) ?>
+
+ +
+ + + + +
+
+ + + diff --git a/phpstan-baseline-demos.php b/phpstan-baseline-demos.php index c2071f2aa..fd8dd7774 100644 --- a/phpstan-baseline-demos.php +++ b/phpstan-baseline-demos.php @@ -85,29 +85,23 @@ 'count' => 1, 'path' => __DIR__ . '/demos/Demo/Example/UserFluentDemo.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$string of function htmlspecialchars expects string, string\\|false given\\.$#', - 'identifier' => 'argument.type', - 'count' => 1, - 'path' => __DIR__ . '/demos/Demo/Highlighter.php', -]; $ignoreErrors[] = [ 'message' => '#^Cannot call method setPassword\\(\\) on mixed\\.$#', 'identifier' => 'method.nonObject', 'count' => 1, - 'path' => __DIR__ . '/demos/index.php', + 'path' => __DIR__ . '/demos/Demo/Showcase/ShowcaseCatalog.php', ]; $ignoreErrors[] = [ 'message' => '#^Cannot call method setSurname\\(\\) on mixed\\.$#', 'identifier' => 'method.nonObject', 'count' => 1, - 'path' => __DIR__ . '/demos/index.php', + 'path' => __DIR__ . '/demos/Demo/Showcase/ShowcaseCatalog.php', ]; $ignoreErrors[] = [ 'message' => '#^Instanceof between Demo\\\\Example\\\\UserFluentDemo and Go\\\\Aop\\\\Proxy will always evaluate to false\\.$#', 'identifier' => 'instanceof.alwaysFalse', 'count' => 1, - 'path' => __DIR__ . '/demos/index.php', + 'path' => __DIR__ . '/demos/Demo/Showcase/ShowcaseCatalog.php', ]; return ['parameters' => ['ignoreErrors' => $ignoreErrors]];