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]];