Skip to content

Dev#51

Merged
Nejcc merged 19 commits into
masterfrom
dev
May 28, 2026
Merged

Dev#51
Nejcc merged 19 commits into
masterfrom
dev

Conversation

@Nejcc

@Nejcc Nejcc commented May 27, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Improved JSON/XML responses with proper content-types/status handling.
    • Expanded scaffolding (helpers, facades, views) and CLI delegation to the central scaffolder.
    • Richer debug 500 error page and a new /testerror page/component.
    • New class-detection helper and instance-based validation result object.
  • Bug Fixes

    • Redirects now use HTTP 303 semantics.
    • Heartbeat now prevents caching.
  • Removals

    • Legacy image/init flows and several scaffold templates removed.
  • Documentation

    • Docs and site copy updated to require PHP 8.5+.

Review Change Stack

Nejcc added 3 commits May 27, 2026 11:05
* Delete Core/Instances.php (38 LoC). Documented as the predecessor of
  Container; Container is its declared 'strict superset'. Only callers
  were 2 contract tests, migrated to instantiate Container directly.
* Kill the broken Response::json(string $payload) — was wired backwards
  (json_decode instead of json_encode, malformed header) and never
  called. Delete the placeholder Response::xml() too (returned the
  input wrapped in spaces).
* Add Response::json(mixed $body, int $code = 200, int $flags = …):
  string — single chokepoint for the (setCode → setHeader → json_encode)
  ritual. Already-encoded strings pass through verbatim. HeartbeatController
  and ScaffoldController now route their JSON paths through it.
* Add is_class(mixed): bool global (Core/helpers.php). Replaces 9 copies
  of `is_string($x) && class_exists($x)` across Db.php (5 sites) and
  Heartbeat.php (4 sites, including the negated form).
* ScaffoldController: stop calling header() / http_response_code()
  directly — go through $response. Drop the private json() helper now
  that Response::json() exists. Extract a small redirect() helper for
  the 4 303 paths that previously each set their own header+code.
…n/xml

Restores XML as a first-class output format alongside JSON — the stubs I
deleted earlier were genuinely broken (Response::xml returned its input
wrapped in spaces) but the *idea* of supporting both for API clients is
valid, so put it back properly.

* Response::xml(string|array|SimpleXMLElement, code, rootElement)
  mirrors Response::json(). Pre-rendered strings pass through;
  SimpleXMLElement gets ->asXML(); arrays recursively serialize under
  a configurable root element. Numeric keys become <item index="N">
  so the output stays well-formed; element names get sanitised.
* Response::send() Accept negotiation now matches application/xml and
  text/xml in addition to the existing application/json / text/html /
  text/* / */*.
* The body dispatch in send() gains an XML arm: strings pass through,
  SimpleXMLElement uses asXML(), RenderInterface emits its data(),
  arrays/objects serialize via Response::xml().
Two new test files locking in the surface added during the DRY pass:

* tests/Unit/Framework/Http/ResponseTest.php (11 tests)
  - json(): array encode + headers, custom status code, string passthrough,
    JSON_UNESCAPED_SLASHES flag respected
  - xml(): array serialisation with root element, custom status code,
    pre-rendered string passthrough, SimpleXMLElement input,
    numeric-key indexed items, invalid-element-name sanitisation,
    nested array roundtrip

* tests/Unit/Framework/Core/HelpersTest.php (7 tests)
  - is_class(): true for existing class, false for unknown string,
    non-string types, function names (functions aren't classes)
  - env(): default fallback, true/false/null/empty sentinel coercion,
    raw string passthrough

Full suite stays green: 101 tests, 218 assertions.
@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR converts several static/shared APIs to container-resolved instances: Response gains JSON/XML builders, Validator becomes instance-based returning ValidationResult, View state moves to ViewRegistry, Scaffolder expands artifact types and removes env gating, Ghost engine and CLI are refactored, DB debug becomes instance-scoped, and the platform requirement is raised to PHP 8.5.

Changes

HTTP Response, Validation, and View Architecture

Layer / File(s) Summary
Response API: JSON/XML builders and content negotiation
packages/silverengine/core/src/Http/Response.php
Introduces Response::json(mixed, int, int) and Response::xml(mixed, int, string) methods that set HTTP status code, Content-Type headers (with charset), and return encoded payloads. Expands content negotiation to accept application/xml and text/xml alongside JSON and HTML, with new dispatch branch for XML rendering including recursive array serialization and SimpleXMLElement conversion.
Validator refactoring and ValidationResult
packages/silverengine/core/src/Http/ValidationResult.php, packages/silverengine/core/src/Http/Validator.php
New ValidationResult final readonly value object stores per-field error maps and exposes passes()/fails() predicates, forField()/all()/toArray() accessors. Validator converted from static check() with shared $data/$errors state to instance method that parses rules, dispatches to instance rule methods via dynamic call, aggregates errors, and returns ValidationResult; removed static get()/pass() API.
Class detection helper and framework adoption
packages/silverengine/core/src/Core/helpers.php, packages/silverengine/core/src/Support/Heartbeat.php, packages/silverengine/core/src/Database/Db.php
New is_class(mixed): bool global helper centralizes "string + class_exists()" checks. Heartbeat middleware/provider validation updated from is_string/class_exists to is_class() in exercise/check phases. Db model/style detection in prepareSelect/setQueryMode/transformResult/fetch/fetchAll migrated to is_class() checks.
View and ViewRegistry: static state to instance-based
packages/silverengine/core/src/Http/View.php, packages/silverengine/core/src/Http/ViewRegistry.php, packages/silverengine/core/src/Core/App.php
New ViewRegistry singleton service manages shared data and composer callbacks previously held in View static properties. View public static APIs (share/shared/composer/flushShared/sharedFor) delegate to container-resolved ViewRegistry instance via private registry() accessor. ViewRegistry and Validator registered as singletons in App::bindFrameworkDefaults().
Controller integration: Heartbeat and Scaffold responses
packages/silverengine/core/src/App/Controllers/HeartbeatController.php, packages/silverengine/core/src/App/Controllers/ScaffoldController.php
HeartbeatController sets Cache-Control header, branches on wantsHtml(), returns json() with flags for non-HTML. ScaffoldController accepts Response parameter, uses $response->json() for errors/exceptions, implements redirect() helper setting 303 status and Location header.
Response, Validator, and Helper comprehensive tests
tests/Unit/Framework/Http/ResponseTest.php, tests/Unit/Framework/Http/ValidatorTest.php, tests/Unit/Framework/Core/HelpersTest.php, tests/Unit/Framework/Core/ContainerContractTest.php
ResponseTest covers json() array encoding, status/header setting, pre-encoded string passthrough, flags respect; xml() array serialization with root element, numeric key handling via indexed items, element-name sanitization, SimpleXMLElement conversion. ValidatorTest refactored to instance with setUp() creating fresh Validator, tests chained rules, field queries, result independence. HelpersTest covers is_class() and env(). ContainerContractTest migrated from Instances to Container.

Scaffolder Expansion and CLI Refactoring

Layer / File(s) Summary
Scaffolder: new artifact types and name suggestion
packages/silverengine/core/src/Support/Scaffolder.php
TYPES constant expanded to include view, helper, facade. Removed guardEnv() calls from create/remove/scaffold/unscaffold, eliminating APP_ENV=local + APP_DEBUG=true requirement. Added viewStub/helperStub/facadeStub generators. Rewrote suggestName() to preserve internal capitals while normalizing snake_case/kebab-case/camelCase to StudlyCase, defaulting to Page on empty input.
CLI: refactored make and delete via Scaffolder
packages/silverengine/core/src/Engine/CLI.php
CLI::make() and CLI::delete() delegated to Scaffolder::create() and Scaffolder::remove(). Updated help text to list new types and simplified syntax. RuntimeException caught and surfaced via CLI error formatter. Removed legacy template-based scaffolding, resource-specific delete flows, and helper methods from CLI class.
Scaffolder tests: types, artifact creation, name normalization
tests/Unit/Framework/Support/ScaffolderTypesTest.php
Comprehensive suite validates TYPES constant, file generation for helper/facade/view with correct namespaces and paths, artifact removal, unknown-type error handling, suggestName() normalization across input formats (preserves SmokeModel, converts snake_case/camelCase to StudlyCase, defaults to Page).

Ghost Template Engine: Compiler and Template Refactoring

Layer / File(s) Summary
Compiler: freshDeps() single-pass cache validation
packages/silverengine/core/src/Engine/Ghost/Compiler.php
New freshDeps(cacheFile, sourceFile): ?array validates cache presence, compiler version, source mtime, dependency mtimes in one pass and returns cached deps map on success. isFresh() marked deprecated, now delegates to freshDeps() to avoid redundant header reads.
Template: unified control filtering and view resolution
packages/silverengine/core/src/Engine/Ghost/Template.php
render() fast-path uses freshDeps() to obtain and bubble dependencies immediately on cache hit. Consolidated filterIf/filterForeach/filterFor into unified filterControl(tag) regex dispatcher. parseMaster() removed str_replace normalization, now uses resolveView() for lookup. New resolveView(alias, subdir): ?string centralizes dotted-alias → absolute-path with .ghost.php preference. Removed Route import and getRoute() method.

Facade Reorganization and Configuration Updates

Layer / File(s) Summary
Facade relocation and Validator facade
packages/silverengine/core/src/Support/Facades/FakeFactory.php, .../Log.php, .../Request.php, .../Response.php, .../Validator.php, packages/silverengine/core/src/Core/Config.php, packages/silverengine/core/src/Core/ErrorHandler.php
FakeFactory, Log, Request, Response facades relocated from Silver\Core\Bootstrap\Facades to Silver\Support\Facades. New Validator facade in Support\Facades delegates to container-resolved Validator::class. Config docblock and ErrorHandler import updated to reference new facade locations.
Config file header removal
packages/silverengine/core/src/Config/Alias.php, .../App.php, .../Middlewares.php, .../Routes.php, .../lang.php
Removed PHPDoc header comment blocks from config files, letting them immediately expose return arrays for clarity.

Database Debug State, Query Builders, and Helper Refactoring

Layer / File(s) Summary
ConnectionManager: per-instance debug flag management
packages/silverengine/core/src/Database/ConnectionManager.php, packages/silverengine/core/src/Database/Db.php
ConnectionManager gains private $debug property with setDebug(bool) and isDebug() methods. Db removes static $global_debug, delegates debugMode() to cm()->setDebug(), updates isDebug() to use per-instance $this->debug when set else cm()->isDebug(), switches exec() logging condition to cm()->isDebug().
Query builders: DELETE GROUP BY removal and WHERE boolean grouping
packages/silverengine/core/src/Database/Query/Delete.php, packages/silverengine/core/src/Database/Traits/QueryWH.php
Delete query builder removes QueryGroupBy trait and omits compileGroupBy/compileHaving from compile(), with docblock explaining standard SQL DELETE does not support those clauses. QueryWH paren() helper rewritten to preserve target slot name, accumulate callback-built group, wrap in Paren with optional NOT, reattach with boolean connector; havingParen/whereParen now delegate to rewritten paren().

PHP 8.5 Platform Upgrade and Documentation

Layer / File(s) Summary
PHP 8.5: version upgrades and manifest changes
composer.json, packages/silverengine/core/composer.json, README.md, packages/silverengine/core/src/Support/Heartbeat.php
Updated composer.json require.php from >=8.4 to >=8.5. Bumped Heartbeat::MIN_PHP from 8.4.0 to 8.5.0. Updated README badges, Requirements section, and Contributing architecture notes to reference PHP 8.5.
Documentation and demo content: PHP 8.5
public/docs/index.html, packages/silverengine/core/src/App/Views/demo/default.ghost.php, app/Resources/js/Pages/About.vue, app/Resources/js/Pages/Welcome.vue
Updated public/docs Introduction/Conventions topics and Heartbeat example JSON to reference PHP 8.5.0. Updated demo template meta description and About/Welcome Vue pages to mention PHP 8.5 instead of 8.4.
Test fixture: TestErrorController, TestError page, and template cleanup
app/Controllers/TestErrorController.php, app/Resources/js/Pages/TestError.vue, app/Routes/Web.php, app/Templates/View.ghost.tpl
Added TestErrorController with __invoke() returning wisp'TestError' response with scaffolded message. Created TestError.vue Inertia page with message prop, navigation, footer. Registered /testerror GET route. Removed View.ghost.tpl template content.

🎯 4 (Complex) | ⏱️ ~90 minutes

Possibly Related PRs

  • SilverEngine/Framework#49: Shared heartbeat/scaffolder endpoint and controller refactoring through Response and redirect handling updates.
  • SilverEngine/Framework#48: Connected database-layer changes via Db/ConnectionManager debug handling delegation and is_class() adoption patterns.
  • SilverEngine/Framework#47: Part of PHP 8.5 modernization effort where this PR builds on the framework's platform constraint upgrade.

🐰 A rabbit scampers through the code so spry,

Moved statics to instances with a curious eye.
JSON and XML now neatly play,
PHP 8.5 leads the bright new day.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

Nejcc added 2 commits May 27, 2026 11:29
Three independent cleanups in one commit; together they remove 4 files,
strip ~1 KB of legacy boilerplate from configs, and convert the
Validator's silently-lying behaviour into loud-fail.

* Delete Init/Image.php + Init/Silver.php + the (unrouted) SystemController
  that called them. The 'framework self-update' mechanism they implemented
  was hard-wired to filesystem paths that don't exist post-modernization
  (System/init/images/, URL.'/public/images.zip'). Nothing in the codebase
  references SystemController anymore.

* Strip the 2015-2017 'SilverEngine Team' phpdoc block from Alias.php /
  App.php / Middlewares.php / Routes.php / lang.php. Pure noise — every
  file was reduced by ~205 bytes of identical boilerplate.

* Delete Http\Validator::checkExist + checkUnique stubs. Both were
  literal 'return false;' — meaning rules like 'email' => 'required|unique'
  silently passed every value, giving callers false confidence the check
  was happening. They aren't. Without the stubs, attempting to use those
  rule names now throws (method-not-found), which is the correct outcome
  for an unimplemented validator. Issue #19 closed as obsolete by deletion.

* Add tests/Unit/Framework/Http/ValidatorTest.php — locks in the rules
  that DO work (required, min, max, match), the chained-rule behaviour
  the .pass() / .get() helpers, AND the new contract that unimplemented
  rule names fail loud rather than silent-passing.

Suite: 115 tests / 240 assertions / 1 pre-existing skip, all green.
BREAKING — the framework already uses PHP 8.5 natives (array_find,
array_any) and would fatal on 8.4 at first request. Make the constraint
honest and align all the user-facing surface.

* composer.json (root + packages/silverengine/core) → "php": ">=8.5"
* Heartbeat::MIN_PHP → '8.5.0' (the runtime self-check now demands what
  the code actually requires)
* README, CLAUDE.md, About.vue, Welcome.vue, default.ghost.php — every
  '8.4+' callout updated to '8.5+'
* public/docs/index.html — body text + the example heartbeat JSON
  snippet bumped for consistency
* README: drop the now-stale 'legacy Instances registry preserved'
  paragraph (the class was deleted in eec1dae; Container kept the same
  method names, but Instances the class no longer exists)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/silverengine/core/src/Http/Response.php`:
- Line 93: Response::xml() currently injects $rootElement directly into the
SimpleXMLElement constructor which can break XML; validate and sanitize
$rootElement before use (e.g., in Response::xml()) by: check against a safe
XML-name pattern (like /^[A-Za-z_][A-Za-z0-9\-\._]*$/), if it fails replace
invalid characters with '_' and ensure the first character is a letter or '_'
(prepend 'root_' if needed), then use the sanitized name when building the
SimpleXMLElement instead of the raw $rootElement.
- Line 65: Response::json() and the application/json branches in send()
currently cast json_encode() results to string, which hides encoding failures;
change both Response::json() and the json/array/object handling in send() to
check json_encode() return value !== false, and on failure throw a
RuntimeException (or return a 500 error) including json_last_error_msg() and
json_last_error() so callers get explicit errors instead of empty bodies; also
update Response::xml() to validate/sanitize the $rootElement (e.g., allow only
valid XML name characters or replace/escape invalid ones and ensure it’s
non-empty) before passing it to new SimpleXMLElement to prevent
injection/invalid-name errors.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9d530e3c-cdc6-41f4-8dbd-8ba2f472e5d7

📥 Commits

Reviewing files that changed from the base of the PR and between 78fefbe and 6b8e7a4.

📒 Files selected for processing (20)
  • packages/silverengine/core/src/App/Controllers/HeartbeatController.php
  • packages/silverengine/core/src/App/Controllers/ScaffoldController.php
  • packages/silverengine/core/src/App/Controllers/SystemController.php
  • packages/silverengine/core/src/Config/Alias.php
  • packages/silverengine/core/src/Config/App.php
  • packages/silverengine/core/src/Config/Middlewares.php
  • packages/silverengine/core/src/Config/Routes.php
  • packages/silverengine/core/src/Config/lang.php
  • packages/silverengine/core/src/Core/Instances.php
  • packages/silverengine/core/src/Core/helpers.php
  • packages/silverengine/core/src/Database/Db.php
  • packages/silverengine/core/src/Http/Response.php
  • packages/silverengine/core/src/Http/Validator.php
  • packages/silverengine/core/src/Init/Image.php
  • packages/silverengine/core/src/Init/Silver.php
  • packages/silverengine/core/src/Support/Heartbeat.php
  • tests/Unit/Framework/Core/ContainerContractTest.php
  • tests/Unit/Framework/Core/HelpersTest.php
  • tests/Unit/Framework/Http/ResponseTest.php
  • tests/Unit/Framework/Http/ValidatorTest.php
💤 Files with no reviewable changes (10)
  • packages/silverengine/core/src/Config/Middlewares.php
  • packages/silverengine/core/src/Config/Alias.php
  • packages/silverengine/core/src/Init/Silver.php
  • packages/silverengine/core/src/Config/Routes.php
  • packages/silverengine/core/src/Http/Validator.php
  • packages/silverengine/core/src/Config/App.php
  • packages/silverengine/core/src/Core/Instances.php
  • packages/silverengine/core/src/Init/Image.php
  • packages/silverengine/core/src/App/Controllers/SystemController.php
  • packages/silverengine/core/src/Config/lang.php

): string {
$this->setCode($code);
$this->setHeader('Content-Type', 'application/json; charset=utf-8');
return is_string($body) ? $body : (string) json_encode($body, $flags);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the file
ls -la packages/silverengine/core/src/Http/Response.php

# Print the relevant sections with line numbers (around json(), xml(), and send())
sed -n '1,220p' packages/silverengine/core/src/Http/Response.php | nl -ba | sed -n '1,220p'

Repository: SilverEngine/Framework

Length of output: 225


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/silverengine/core/src/Http/Response.php"
echo "== $FILE =="
wc -l "$FILE"

echo "== json() and related around line 55-120 =="
sed -n '40,130p' "$FILE" | cat -n

echo "== xml() and any root element validation around line 80-170 =="
sed -n '80,190p' "$FILE" | cat -n

echo "== send() / content-type negotiation around line 130-280 =="
sed -n '120,280p' "$FILE" | cat -n

Repository: SilverEngine/Framework

Length of output: 14724


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "->xml\(" packages/silverengine/core/src | head -n 50
rg -n "Response::xml\(" packages/silverengine | head -n 50
rg -n "function xml\(" packages/silverengine/core/src/Http/Response.php
rg -n "\$rootElement" packages/silverengine/core/src | head -n 50

Repository: SilverEngine/Framework

Length of output: 471


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -- '->xml\(' packages/silverengine/core/src | head -n 50
rg -n -- 'Response::xml\(' packages/silverengine | head -n 50
rg -n -- '\$rootElement' packages/silverengine/core/src | head -n 50

# Also find xml() call sites that pass 3rd arg (root element)
rg -n -- '->xml\([^)]*,[^)]*,[^)]*\)' packages/silverengine/core/src | head -n 50
rg -n -- '->xml\([^)]*,[^)]*,[^)]*\)' packages/silverengine | head -n 50

Repository: SilverEngine/Framework

Length of output: 331


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find all xml() method calls on Response instances
rg -n -- '->xml\(' packages | head -n 200

# Find xml() calls that pass 3 arguments (body, code, rootElement)
rg -n -- '->xml\([^,]+,[^,]+,[^)]+\)' packages | head -n 200

# Also check direct calls via $this->xml(...) with explicit third arg
rg -n -- '\$this->xml\([^,]+,[^,]+,[^)]+\)' packages | head -n 200

# Check for named argument usage that could set rootElement
rg -n -- '->xml\([^)]*rootElement\s*=>' packages | head -n 200

Repository: SilverEngine/Framework

Length of output: 331


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="packages/silverengine/core/src/Http/Response.php"

python3 - <<'PY'
import itertools
path = "packages/silverengine/core/src/Http/Response.php"
with open(path,'r',encoding='utf-8') as f:
    lines = f.readlines()

targets = [62,63,64,65,66,67,68,69,70,85,90,93,94]  # around json and near where root might appear
for i in targets:
    if 1 <= i <= len(lines):
        print(f"{i}: {lines[i-1].rstrip()}")
PY

Repository: SilverEngine/Framework

Length of output: 671


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show all xml() call sites (Response instances / other helpers)
rg -n -- '->xml\(' packages/silverengine | head -n 200
rg -n -- '\$this->xml\(' packages/silverengine | head -n 200
rg -n -- 'Response::xml\(' packages/silverengine | head -n 200

# Try to catch 3-arg calls more robustly (works best for simple single-line calls)
rg -n -- '->xml\([^()]*,[^()]*,[^()]*\)' packages/silverengine | head -n 200
rg -n -- '\$this->xml\([^()]*,[^()]*,[^()]*\)' packages/silverengine | head -n 200

Repository: SilverEngine/Framework

Length of output: 596


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Matches for '->xml(' =="
rg -n -- '->xml\(' packages | wc -l
rg -n -C 2 -- '->xml\(' packages | head -n 200

echo "== Matches for '\$this->xml(' =="
rg -n -- '\$this->xml\(' packages | wc -l
rg -n -C 2 -- '\$this->xml\(' packages | head -n 200

echo "== Matches for 'Response::xml(' =="
rg -n -- 'Response::xml\(' packages | wc -l
rg -n -C 2 -- 'Response::xml\(' packages | head -n 200

Repository: SilverEngine/Framework

Length of output: 1568


Handle JSON encoding failures explicitly.

  • Response::json() currently casts json_encode() to string: (string) json_encode(...), which can silently produce an empty body on encoding failure while still returning a successful response code.
  • send() also contains unchecked json_encode() calls for application/json (and array/object cases), so failures can still result in empty output.
  • Response::xml() interpolates $rootElement directly into the SimpleXMLElement constructor; within this repo it’s only called with the default root element, but validate/sanitize $rootElement for external callers.
Proposed fix
-        return is_string($body) ? $body : (string) json_encode($body, $flags);
+        if (is_string($body)) {
+            return $body;
+        }
+        return json_encode($body, $flags | JSON_THROW_ON_ERROR);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/silverengine/core/src/Http/Response.php` at line 65,
Response::json() and the application/json branches in send() currently cast
json_encode() results to string, which hides encoding failures; change both
Response::json() and the json/array/object handling in send() to check
json_encode() return value !== false, and on failure throw a RuntimeException
(or return a 500 error) including json_last_error_msg() and json_last_error() so
callers get explicit errors instead of empty bodies; also update Response::xml()
to validate/sanitize the $rootElement (e.g., allow only valid XML name
characters or replace/escape invalid ones and ensure it’s non-empty) before
passing it to new SimpleXMLElement to prevent injection/invalid-name errors.

return (string) $body->asXML();
}

$root = new \SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><' . $rootElement . '/>');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/silverengine/core/src/Http/Response.php"

echo "== File exists =="
ls -l "$FILE"

echo
echo "== Context around line 93 =="
nl -ba "$FILE" | sed -n '60,120p'

echo
echo "== Find all usages of \$rootElement in this file =="
rg -n "\$rootElement" "$FILE"

echo
echo "== Trace where rootElement is assigned in this file (near occurrences) =="
# Print a bit of context around each assignment
rg -n "\$rootElement\s*=" "$FILE" -n || true
# Better: show previous/next 30 lines around matches
for m in $(rg -n "\$rootElement" "$FILE" | cut -d: -f1 | sort -n | uniq); do
  echo
  echo "--- Around line $m ---"
  nl -ba "$FILE" | sed -n "$((m-25)),$((m+25))p"
done

Repository: SilverEngine/Framework

Length of output: 274


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/silverengine/core/src/Http/Response.php"

echo "== Context around line 93 (numbered via awk) =="
awk 'NR>=60 && NR<=120 {printf "%6d\t%s\n", NR, $0}' "$FILE"

echo
echo "== Find all usages of \$rootElement in this file =="
rg -n "\$rootElement" "$FILE"

echo
echo "== Show assignments to \$rootElement =="
rg -n "\$rootElement\s*=" "$FILE" || true

echo
echo "== Show any parsing/validation of root element around usages =="
# Search nearby for common patterns
rg -n "(SimpleXMLElement|sanitize|preg_match|preg_replace|xml|rootElement)" "$FILE"

Repository: SilverEngine/Framework

Length of output: 3015


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/silverengine/core/src/Http/Response.php"

echo "== Search for 'rootElement' (no $) =="
rg -n "rootElement" "$FILE" || true

echo
echo "== Search for 'function xml(' in Response.php =="
rg -n "function xml\(" "$FILE"

echo
echo "== Search for calls to ->xml(...) passing 3rd argument (root element) =="
rg -n "->xml\([^,]+,[^,]+,[^)]+\)" -S . || true

echo
echo "== Search for calls to xml(...) with 3 arguments (root element) =="
rg -n "\bxml\([^,]+,[^,]+,[^)]+\)" -S . || true

Repository: SilverEngine/Framework

Length of output: 1103


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/silverengine/core/src/Http/Response.php"
TEST="tests/Unit/Framework/Http/ResponseTest.php"

echo "== Response.php: xml() and arrayToXml() (lines 70-150) =="
awk 'NR>=70 && NR<=160 {printf "%6d\t%s\n", NR, $0}' "$FILE"

echo
echo "== ResponseTest.php: around line 62 =="
awk 'NR>=1 && NR<=120 {printf "%6d\t%s\n", NR, $0}' "$TEST"

echo
echo "== grep/rg for 'envelope' usage in tests =="
rg -n "envelope" "$TEST" || true

Repository: SilverEngine/Framework

Length of output: 9090


Sanitize $rootElement before using it as the XML root element name in Response::xml()

$rootElement is a Response::xml() parameter and is currently concatenated directly into SimpleXMLElement('<?xml...?><' . $rootElement . '/>'), so invalid characters or names can make the generated XML malformed at runtime.

Proposed fix
-        $root = new \SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><' . $rootElement . '/>');
+        $name = preg_replace('/[^A-Za-z0-9_\-]/', '_', $rootElement) ?: 'response';
+        if (!preg_match('/^[A-Za-z_]/', $name)) {
+            $name = '_' . $name;
+        }
+        $root = new \SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><' . $name . '/>');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$root = new \SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><' . $rootElement . '/>');
$name = preg_replace('/[^A-Za-z0-9_\-]/', '_', $rootElement) ?: 'response';
if (!preg_match('/^[A-Za-z_]/', $name)) {
$name = '_' . $name;
}
$root = new \SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><' . $name . '/>');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/silverengine/core/src/Http/Response.php` at line 93, Response::xml()
currently injects $rootElement directly into the SimpleXMLElement constructor
which can break XML; validate and sanitize $rootElement before use (e.g., in
Response::xml()) by: check against a safe XML-name pattern (like
/^[A-Za-z_][A-Za-z0-9\-\._]*$/), if it fails replace invalid characters with '_'
and ensure the first character is a letter or '_' (prepend 'root_' if needed),
then use the sanitized name when building the SimpleXMLElement instead of the
raw $rootElement.

Nejcc added 3 commits May 27, 2026 11:40
BREAKING — CLI 'g/d' commands now produce modern stubs (final, strict
types, single-action __invoke controllers, Wisp-aware pages). Anything
relying on the old multi-method (get/post/put/delete) controller stub
or the ReflectORM-style model properties (selectable/hidden/etc) needs
to update. Output paths are unchanged for the types CLI already had.

Single source of truth: Silver\Support\Scaffolder owns every stub
shape. The web /new UI and 'php silver g' both call Scaffolder::create()
+ ::remove() — no second implementation, no second set of stubs.

* Scaffolder: add three more types (view / helper / facade) so the
  service covers CLI's full surface. view writes a Ghost template under
  app/Resources/views/, helper writes a final class under app/Helpers/,
  facade extends Silver\Support\Facade pointed at <Name>Service.
* Scaffolder: drop guardEnv() — the local+debug gate was duplicated by
  the HTTP controllers (ScaffoldController, NewController) that wrap it.
  Removing it lets 'php silver g foo' work in any environment locally
  (the web endpoints stay gated by their controllers).
* CLI: delete ~140 LoC of generate/deleteResources/resolvePaths/
  generateFile/generateView/fixRoutes. Replaced by ~25 lines that
  delegate to Scaffolder + render per-file 'created/removed' progress.
* CLI: help text bumped to list every type Scaffolder supports.
* Delete legacy app/Templates/*.tpl (7 files). They contained stale
  pre-Wisp / pre-ReflectORM-removal templates that have been emitting
  broken code for the last modernization cycle.
* tests/Unit/Framework/Support/ScaffolderTypesTest.php — locks in
  view/helper/facade stub shape + on-disk locations + the TYPES
  constant list.

Suite: 120 tests / 262 assertions / 1 skipped, all green.
Studly-casing pipeline used to strtolower(every-part) then ucfirst — so
already-PascalCase inputs got their internal capitals erased:

  Before: 'SmokeModel'  -> 'Smokemodel.php'  (CLI smoke test surfaced this)
  After:  'SmokeModel'  -> 'SmokeModel.php'  ✓

Split on the lowercase->uppercase boundary too (plus the ALL_CAPS->Word
boundary so e.g. 'XMLParser' splits cleanly). Existing URL slug behaviour
('foo-bar-baz' -> 'FooBarBaz') and snake_case ('user_post' -> 'UserPost')
are unchanged.

+4 tests in ScaffolderTypesTest pinning the new behaviour.
Four small wins inside the Ghost template engine, found via a focused
DRY audit:

* Template::resolveView($alias, $subdir) — extracts the dotted-alias
  -> 'app/Views/{subdir}{path}.ghost.{php,tpl}' fallback. Used to live
  inline in parseMaster(), includeFile() and includeComponent(); all
  three now go through the same helper. Single point to evolve view
  lookup later (e.g. searching multiple roots).

* Compiler::freshDeps($cacheFile, $sourceFile) — single-pass replacement
  for the prior 'isFresh() then depsFrom()' two-call pattern that opened
  and parsed the same cache header file twice on every cache hit.
  isFresh() kept as a one-line shim over freshDeps(); depsFrom() kept
  with a @deprecated note for callers that only need deps.

* Template::filterControl($body, $tag) — table-drives the
  #if/#foreach/#for filters that were three near-identical methods.
  #if branch additionally handles #elseif/#else; the other tags just
  open and close. Three methods (~20 LoC) -> one (~15 LoC).

* Delete Template::getRoute() — dead code, no callers in the codebase.
  Also drops the now-unused 'use Silver\Core\Route' import.

DeferProp / LazyProp twins audited too; deliberately not merged.
Same shape, but they encode genuinely different Inertia protocols
(deferred = autoload after mount; lazy = only on explicit fetch).
A shared base would save ~4 LoC at the cost of hiding the protocol
distinction the class names + docblocks currently make explicit.

Suite: 124 tests, 272 assertions, 1 pre-existing skip. All green.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
tests/Unit/Framework/Support/ScaffolderTypesTest.php (1)

38-54: ⚡ Quick win

Test may leave artifacts on failure.

If any assertion before remove() fails, the test file (app/Helpers/Smoke.php) will remain on disk. Consider using try/finally or tearDown() to ensure cleanup.

♻️ Ensure cleanup with try/finally
     public function testCreateHelperWritesFile(): void
     {
         $result = $this->scaffolder->create('helper', 'Smoke');
-        $this->assertSame('helper', $result['type']);
-        $this->assertSame('Smoke', $result['name']);
-        $this->assertSame(['app/Helpers/Smoke.php'], $result['created']);
-
         $path = ROOT . 'app/Helpers/Smoke.php';
-        $this->assertFileExists($path);
-        $src = (string) file_get_contents($path);
-        $this->assertStringContainsString('namespace App\\Helpers', $src);
-        $this->assertStringContainsString('final class Smoke', $src);
-        $this->assertStringContainsString('declare(strict_types=1)', $src);
-
-        $this->scaffolder->remove('helper', 'Smoke');
-        $this->assertFileDoesNotExist($path);
+        try {
+            $this->assertSame('helper', $result['type']);
+            $this->assertSame('Smoke', $result['name']);
+            $this->assertSame(['app/Helpers/Smoke.php'], $result['created']);
+
+            $this->assertFileExists($path);
+            $src = (string) file_get_contents($path);
+            $this->assertStringContainsString('namespace App\\Helpers', $src);
+            $this->assertStringContainsString('final class Smoke', $src);
+            $this->assertStringContainsString('declare(strict_types=1)', $src);
+        } finally {
+            `@unlink`($path);
+        }
     }

The same pattern applies to testCreateFacadeWritesFile and testCreateViewWritesGhostTemplate.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Unit/Framework/Support/ScaffolderTypesTest.php` around lines 38 - 54,
The testCreateHelperWritesFile (and similarly testCreateFacadeWritesFile and
testCreateViewWritesGhostTemplate) can leave app/Helpers/Smoke.php (and other
scaffolded files) on disk if an assertion fails before
$this->scaffolder->remove() is called; wrap the creation and assertions in a
try/finally (or move cleanup into tearDown()) so
$this->scaffolder->remove('helper','Smoke') always runs (use the same $path
variable and the scaffolder->remove calls referenced in the test to perform
cleanup), and apply the same pattern to the facade and view tests to guarantee
files are deleted even on failure.
packages/silverengine/core/src/Support/Scaffolder.php (1)

637-650: 💤 Low value

Generated view template contains leading indentation.

The heredoc preserves the 12-space indentation, so the generated .ghost.tpl file will have each line indented. While functional, this differs from other stubs which start content at column 0. Consider using a non-indented heredoc for consistency.

♻️ Optional: Remove heredoc indentation
     private function viewStub(string $name): string
     {
         $title = $name;
-        return <<<TPL
-            {{ extends('layouts.master') }}
-
-            `#set`[content]
-                <h1>{$title}</h1>
-                <p>Welcome to <b>{$title}</b>. Edit this file under
-                <code>app/Resources/views/</code>.</p>
-            `#end`
-
-            TPL;
+        return <<<TPL
+{{ extends('layouts.master') }}
+
+#set[content]
+    <h1>{$title}</h1>
+    <p>Welcome to <b>{$title}</b>. Edit this file under
+    <code>app/Resources/views/</code>.</p>
+#end
+
+TPL;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/silverengine/core/src/Support/Scaffolder.php` around lines 637 -
650, The heredoc in viewStub(string $name) preserves leading indentation causing
generated templates to be indented; update viewStub to return a left-aligned
heredoc (move the opening/lines so the template content starts at column 0 and
the closing TPL marker is at column 0) or alternatively build the string and
call trim() or preg_replace to strip the common leading whitespace from the
returned string; ensure you reference the $title interpolation in the adjusted
heredoc so the generated .ghost.tpl content matches other stubs (no leading
12-space indentation).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/silverengine/core/src/Support/Scaffolder.php`:
- Around line 637-650: The heredoc in viewStub(string $name) preserves leading
indentation causing generated templates to be indented; update viewStub to
return a left-aligned heredoc (move the opening/lines so the template content
starts at column 0 and the closing TPL marker is at column 0) or alternatively
build the string and call trim() or preg_replace to strip the common leading
whitespace from the returned string; ensure you reference the $title
interpolation in the adjusted heredoc so the generated .ghost.tpl content
matches other stubs (no leading 12-space indentation).

In `@tests/Unit/Framework/Support/ScaffolderTypesTest.php`:
- Around line 38-54: The testCreateHelperWritesFile (and similarly
testCreateFacadeWritesFile and testCreateViewWritesGhostTemplate) can leave
app/Helpers/Smoke.php (and other scaffolded files) on disk if an assertion fails
before $this->scaffolder->remove() is called; wrap the creation and assertions
in a try/finally (or move cleanup into tearDown()) so
$this->scaffolder->remove('helper','Smoke') always runs (use the same $path
variable and the scaffolder->remove calls referenced in the test to perform
cleanup), and apply the same pattern to the facade and view tests to guarantee
files are deleted even on failure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b3f2a5f8-e9aa-49b9-837f-9d5938e4f490

📥 Commits

Reviewing files that changed from the base of the PR and between 6b8e7a4 and 9948646.

📒 Files selected for processing (18)
  • README.md
  • app/Resources/js/Pages/About.vue
  • app/Resources/js/Pages/Welcome.vue
  • app/Templates/Controller.ghost.tpl
  • app/Templates/Event.ghost.tpl
  • app/Templates/Facade.ghost.tpl
  • app/Templates/Helper.ghost.tpl
  • app/Templates/Listeners.ghost.tpl
  • app/Templates/Model.ghost.tpl
  • app/Templates/View.ghost.tpl
  • composer.json
  • packages/silverengine/core/composer.json
  • packages/silverengine/core/src/App/Views/demo/default.ghost.php
  • packages/silverengine/core/src/Engine/CLI.php
  • packages/silverengine/core/src/Support/Heartbeat.php
  • packages/silverengine/core/src/Support/Scaffolder.php
  • public/docs/index.html
  • tests/Unit/Framework/Support/ScaffolderTypesTest.php
💤 Files with no reviewable changes (7)
  • app/Templates/View.ghost.tpl
  • app/Templates/Facade.ghost.tpl
  • app/Templates/Controller.ghost.tpl
  • app/Templates/Event.ghost.tpl
  • app/Templates/Model.ghost.tpl
  • app/Templates/Helper.ghost.tpl
  • app/Templates/Listeners.ghost.tpl
✅ Files skipped from review due to trivial changes (6)
  • packages/silverengine/core/composer.json
  • packages/silverengine/core/src/App/Views/demo/default.ghost.php
  • app/Resources/js/Pages/About.vue
  • public/docs/index.html
  • app/Resources/js/Pages/Welcome.vue
  • README.md

Nejcc added 2 commits May 27, 2026 11:50
BREAKING — the validator API moves from `Validator::pass()/get()` shared
static state to a per-call immutable result. This was the single highest-
ROI de-static move in the audit because the old shape had a real bug:

    Validator::check($formA, $rulesA);   // sets static $errors
    Validator::check($formB, $rulesB);   // OVERWRITES it
    Validator::pass();                   // reads $formB's, not $formA's

Two checks in the same request would silently clobber each other.

The new shape:

    $result = Validator::check($data, $rules);
    if ($result->fails()) { $result->forField('email'); }
    $result->passes();
    $result->all();
    $result->toArray();

Three pieces:

* Silver\Http\ValidationResult — new readonly value object holding the
  errors map. passes() / fails() / forField() / hasField() / all() /
  toArray() — every "is this OK?" question lives on the result, not on
  a globally-mutable static.

* Silver\Http\Validator — rewritten as a small instance class. check()
  returns a fresh ValidationResult per call. Per-rule methods now take
  ($value, $data, ...$args) so cross-field rules ('match') can compare
  against sibling values without reaching into static $data.

* Silver\Core\Bootstrap\Facades\Validator — Laravel-style facade so
  the call site `Validator::check(...)` keeps working. Behind __callStatic
  it resolves the instance from the container — tests substitute via
  Container::instance(\Silver\Http\Validator::class, $fake), the
  framework gets one shared resolver, and the result is per-call regardless.

Tests rewritten end-to-end (15 cases, 26 assertions) including the
exact regression case the static state would have failed:
testTwoSequentialChecksDoNotClobberEachOther. Suite: 125 / 276 / 1 skipped.

Why a facade rather than full DI everywhere? The audit honestly mapped
which statics carry hidden state vs which are stateless value/util types
(see chat log). Validator was Category 3 — "hiding state behind static".
Categories 1, 2 and 4 (Env, Parts/, helpers, Wisp::el, Compiler dep
tracking) keep their statics; they don't have this bug class.
…pace

Two related moves in one commit.

A. Kill static state on View (the second de-static target)

   View::$shared and View::$composers used to be private static arrays.
   Two different parts of a request couldn't sensibly register composers
   in isolation, and tests had to manually `View::flushShared()` between
   cases to avoid leaks.

   * Silver\Http\ViewRegistry — new instance class owning shared() /
     share() / composer() / sharedFor() / reset(). Registered as a
     container singleton in App::bindFrameworkDefaults().
   * Silver\Http\View — static surface preserved (share / shared /
     composer / flushShared / sharedFor) but now delegates to the
     container-resolved registry. Every existing call site keeps working
     unchanged; tests can swap a fresh registry via
     Container::instance(ViewRegistry::class, new ViewRegistry()).
   * Also register Silver\Http\Validator as a singleton — required for
     the facade to dispatch consistently across requests.

B. Move facades from Core/Bootstrap/Facades/ → Support/Facades/

   The five facades (FakeFactory, Log, Request, Response, Validator)
   moved next to their base class Silver\Support\Facade. The old
   location lived under 'Bootstrap' which never made sense for these
   call sites.

   * Namespace migration: Silver\Core\Bootstrap\Facades\* →
     Silver\Support\Facades\* (matches naming the user asked for:
     class name without the 'Facade' suffix, 'Facades' in the namespace).
   * Two external callers updated: Core/ErrorHandler.php (Request
     facade import) and tests/Unit/Framework/Http/ValidatorTest.php
     (ValidatorFacade alias). Core/Config.php docblock reference too.

Suite: 125 / 276 / 1 skipped, all green. Routes 200.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/silverengine/core/src/Http/Validator.php`:
- Around line 61-65: The checkRequired method incorrectly treats falsy-but-valid
values (0, "0", false) as missing due to a loose truthiness test; update
checkRequired to only fail when the value is actually absent/empty (e.g., null
or the empty string) instead of any falsy value. In function checkRequired
replace the ternary ($value ? false : 'KEY is required!') with an explicit check
such as if ($value === null || $value === '') return 'KEY is required!'; return
false; so that 0, "0", and false are accepted as present. Ensure you keep the
function signature and name (checkRequired) intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b69c7dfb-4dfb-436c-8e02-8e783fe0e1d4

📥 Commits

Reviewing files that changed from the base of the PR and between 9948646 and 9df4350.

📒 Files selected for processing (6)
  • packages/silverengine/core/src/Core/Bootstrap/Facades/Validator.php
  • packages/silverengine/core/src/Engine/Ghost/Compiler.php
  • packages/silverengine/core/src/Engine/Ghost/Template.php
  • packages/silverengine/core/src/Http/ValidationResult.php
  • packages/silverengine/core/src/Http/Validator.php
  • tests/Unit/Framework/Http/ValidatorTest.php
✅ Files skipped from review due to trivial changes (1)
  • packages/silverengine/core/src/Core/Bootstrap/Facades/Validator.php

Comment on lines +61 to 65
private function checkRequired(mixed $value, array $data): string|false
{
return empty(self::$errors);
unset($data);
return $value ? false : 'KEY is required!';
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

checkRequired treats valid falsy values (like 0 or '0') as missing.

The truthiness check $value ? false : ... will incorrectly fail validation for legitimate values like integer 0, string '0', or false (if that's a valid input). Consider checking for actual absence/emptiness instead.

Proposed fix
     private function checkRequired(mixed $value, array $data): string|false
     {
         unset($data);
-        return $value ? false : 'KEY is required!';
+        return ($value !== null && $value !== '') ? false : 'KEY is required!';
     }
🧰 Tools
🪛 PHPMD (2.15.0)

[warning] 61-65: Avoid unused private methods such as 'checkRequired'. (undefined)

(UnusedPrivateMethod)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/silverengine/core/src/Http/Validator.php` around lines 61 - 65, The
checkRequired method incorrectly treats falsy-but-valid values (0, "0", false)
as missing due to a loose truthiness test; update checkRequired to only fail
when the value is actually absent/empty (e.g., null or the empty string) instead
of any falsy value. In function checkRequired replace the ternary ($value ?
false : 'KEY is required!') with an explicit check such as if ($value === null
|| $value === '') return 'KEY is required!'; return false; so that 0, "0", and
false are accepted as present. Ensure you keep the function signature and name
(checkRequired) intact.

Nejcc added 3 commits May 27, 2026 12:01
… instance

Last static-state slot on Db. The flag itself is tiny (one bool) but
it's the only piece preventing Db from being honestly described as
'fully delegating'.

* ConnectionManager gains setDebug()/isDebug() as instance methods. The
  flag lives on the container-resolved singleton, so a test that swaps
  ConnectionManager via Container::instance(...) gets a fresh debug
  state too — previously a leaky class-wide static.
* Db::debugMode() delegates to the manager; Db::isDebug() reads from it
  (per-Query $this->debug still wins as the local override).
* Db::exec() reads the flag via ConnectionManager instead of a self::
  reference, so all paths agree on one source of truth.

Suite: 125 tests, all green.
Pull two clear improvements out of the Query audit. The deeper bug
(broken paren() variable-shadowing + Parts construction mismatch on
the grouped-where code path) needs its own investigation; this commit
only takes the safe wins.

* Traits/QueryWH.php
  - Add missing 'use Silver\Database\Parts\Paren;' — every Paren
    reference was previously unresolvable.
  - Rename callee in havingParen() / whereParen() from $this->parent()
    (which doesn't exist) to $this->paren() (the method that does).
    Pure typo fix; the grouped-where path still fails further down
    in Parts construction, but at least the first hop is honest now.
  - paren() rewritten to stop shadowing its first parameter
    ($cond = $this->$cond clobbered the slot name with the slot
    value, making every subsequent $this->$cond = … write a no-op).
    The slot name now stays in $condName.
  - Drop unused 'use Silver\Database\Parts\Raw;'.
  - Fix '\Query' instanceof check → proper 'Silver\Database\Query'
    via 'use Silver\Database\Query;'.

* Query/Delete.php
  - Drop QueryGroupBy trait + the two // FIXME-marked compile calls
    (compileGroupBy, compileHaving). Standard DELETE has no GROUP BY
    or HAVING clauses on any driver; emitting them would have produced
    invalid SQL.

Suite: 125 / 276 / 1 skipped.
Triggered by /testerror — the old 500 page was honest but plain: bare
stack frames (vendor/Composer noise at the top), no code context per
frame, no syntax-aware highlighting on the error line, no environment
panel, light-mode only, no editor jumping.

The new view is still inline-CSS-only (must render when the asset build
is dead) but a lot more useful:

  ErrorHandler (Core/ErrorHandler.php)
   * codeAroundLines(file, line, pad) — returns [{n, text, hit}] rows
     so the view can lay out a proper line-numbered table with the
     hit line highlighted (was a flat string before).
   * frameKind(file) — classifies each stack frame as
     app | framework | vendor | internal by source-directory prefix.
   * ideLink(file, line) — emits phpstorm:// or vscode:// (picked by
     $IDE_PROTOCOL env, default phpstorm).
   * normalizeFrames() now returns rich frame metadata per frame:
     where, file, rel, line, kind, ide, and a 6-line source snippet.
   * previousChain() — flattens $e->getPrevious() into a display list
     so wrapped exceptions show their original cause.
   * render() passes the new payload: rel_file, source, source_ide,
     previous, env (php / name / debug / opcache / mem_peak).

  500.ghost.php (App/Views/errors/500.ghost.php)
   * Full rewrite. Dark-mode via prefers-color-scheme using CSS
     custom properties throughout; no JS required.
   * Header band with pulsing accent dot, exception class, message,
     IDE-linked file:line, and right-side meta pills
     (env / php / opcache / memory peak).
   * 'Caused by' section walks getPrevious() and renders each wrapper
     stage with class/message/location.
   * Source viewer: numbered <ol>, the hit line gets a red rail +
     tinted background. Header has an 'open in editor →' deep link.
   * Stack trace: each frame is a <details>/summary. Kind chip
     (app/framework/vendor/internal) on the left, where in the middle,
     file:line on the right (also IDE-linked). Expanding a frame
     reveals its 6-line source snippet inline. Vendor + internal
     frames hidden by default behind a CSS-only toggle so the user
     code stays at eye level.
   * Two-column grid at the bottom: Request (method/URI/route/client/
     query/input) + Environment (env/debug/PHP/opcache/memory).
   * Production branch unchanged shape, just dark-mode aware.

  ErrorPageTest.php
   * The self-contained assertion was checking for the bare strings
     'viteCss' and '/build/' — now false-positives because debug frame
     snippets can legitimately include those tokens from source. Switch
     to specific regex for the *actual* leak pattern:
       <link href=".../build/..."  /  <script src=".../build/..."
     plus an explicit check that the {{ viteCss() }} Ghost directive
     was compiled away.

Suite: 125 / 277 / 1 skipped, all green.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/silverengine/core/src/Http/ViewRegistry.php`:
- Around line 77-85: The anonymous callback passed to array_any in
ViewRegistry:: (the foreach over $this->composers) only declares one parameter
but array_any invokes the callback with (value, key); update the callback
signature to accept two parameters (e.g., static fn(string $pattern, $_): bool
=> ...) so it matches array_any's (value, key) invocation and avoid an
argument-count error, keeping the matching logic the same and leaving the rest
of the foreach (including the array_merge of composer['callback']) unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3d5301f2-df23-4ff8-bd55-97c8170fab17

📥 Commits

Reviewing files that changed from the base of the PR and between 9df4350 and 756f6f1.

📒 Files selected for processing (18)
  • app/Controllers/TestErrorController.php
  • app/Resources/js/Pages/TestError.vue
  • app/Routes/Web.php
  • packages/silverengine/core/src/Core/App.php
  • packages/silverengine/core/src/Core/Config.php
  • packages/silverengine/core/src/Core/ErrorHandler.php
  • packages/silverengine/core/src/Database/ConnectionManager.php
  • packages/silverengine/core/src/Database/Db.php
  • packages/silverengine/core/src/Database/Query/Delete.php
  • packages/silverengine/core/src/Database/Traits/QueryWH.php
  • packages/silverengine/core/src/Http/View.php
  • packages/silverengine/core/src/Http/ViewRegistry.php
  • packages/silverengine/core/src/Support/Facades/FakeFactory.php
  • packages/silverengine/core/src/Support/Facades/Log.php
  • packages/silverengine/core/src/Support/Facades/Request.php
  • packages/silverengine/core/src/Support/Facades/Response.php
  • packages/silverengine/core/src/Support/Facades/Validator.php
  • tests/Unit/Framework/Http/ValidatorTest.php
✅ Files skipped from review due to trivial changes (2)
  • packages/silverengine/core/src/Support/Facades/FakeFactory.php
  • packages/silverengine/core/src/Core/Config.php
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/Unit/Framework/Http/ValidatorTest.php

Comment on lines +77 to +85
foreach ($this->composers as $composer) {
$matches = array_any(
$composer['patterns'],
static fn (string $pattern): bool => $pattern === $name || fnmatch($pattern, $name),
);
if ($matches) {
$data = array_merge($data, (array) ($composer['callback'])($name));
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

PHP 8.4 array_any function

💡 Result:

The array_any function is a standard library function introduced in PHP 8.4.0 that determines if at least one element in an array satisfies a given callback condition [1][2][3]. Key details include: Function Signature bool array_any(array $array, callable $callback) Behavior - The function iterates through the provided array and applies the callback to each element [1][3]. - The callback receives two arguments: the element value and the element key [1][2][3]. - It returns true immediately if the callback returns true for any element, at which point iteration stops [1][3]. - It returns false if the callback does not return true for any element [1][3]. - It returns false if the array is empty [3]. Usage Example $array = ['dog', 'cat', 'elephant']; // Checks if any animal name has more than 5 characters $result = array_any($array, fn(string $value) => strlen($value) > 5); var_dump($result); // Outputs: bool(true) Comparison with Other Functions - Unlike array_filter, which returns an array of elements that match a condition, array_any returns a boolean indicating only the existence of a match [2]. - The callback for array_any strictly requires two arguments (value and key), which can cause errors if used with existing built-in functions that expect a different number of arguments, such as is_integer [2].

Citations:


Fix callback signature for array_any()

array_any() is available in PHP 8.4+ (so PHP 8.5 is fine), but it calls the callback as (value, key). In packages/silverengine/core/src/Http/ViewRegistry.php (lines 77-85), the callback is static fn (string $pattern): bool => ... with only one parameter, which can cause an argument-count error at runtime—adjust it to accept both args (e.g., static fn (string $pattern, $_): bool => ...).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/silverengine/core/src/Http/ViewRegistry.php` around lines 77 - 85,
The anonymous callback passed to array_any in ViewRegistry:: (the foreach over
$this->composers) only declares one parameter but array_any invokes the callback
with (value, key); update the callback signature to accept two parameters (e.g.,
static fn(string $pattern, $_): bool => ...) so it matches array_any's (value,
key) invocation and avoid an argument-count error, keeping the matching logic
the same and leaving the rest of the foreach (including the array_merge of
composer['callback']) unchanged.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/Unit/Framework/Core/ErrorPageTest.php`:
- Around line 58-67: The two assertions in ErrorPageTest.php using
assertDoesNotMatchRegularExpression (the one checking
'#<link[^>]+href="[^"]*/build/#i' and the one checking
'#<script[^>]+src="[^"]*/build/#i') only match double-quoted attributes; update
both regexes to accept either single or double quotes around href/src (e.g.
match quote char and use a complementary class for the value) so they also catch
patterns like href='.../build/...' and src='.../build/...'.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d59210da-9d8c-429d-949b-5792e62e0a5a

📥 Commits

Reviewing files that changed from the base of the PR and between 756f6f1 and e37c2d1.

📒 Files selected for processing (3)
  • packages/silverengine/core/src/App/Views/errors/500.ghost.php
  • packages/silverengine/core/src/Core/ErrorHandler.php
  • tests/Unit/Framework/Core/ErrorPageTest.php

Comment on lines +58 to +67
$this->assertDoesNotMatchRegularExpression(
'#<link[^>]+href="[^"]*/build/#i',
$html,
'View must not <link> any Vite-built CSS.',
);
$this->assertDoesNotMatchRegularExpression(
'#<script[^>]+src="[^"]*/build/#i',
$html,
'View must not <script src> any Vite-built JS.',
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Broaden asset-tag regex to catch single-quoted attributes.

These assertions currently miss <link ... href='.../build/...'> and <script ... src='.../build/...'>, so a leak could slip through.

🔧 Suggested patch
         $this->assertDoesNotMatchRegularExpression(
-            '#<link[^>]+href="[^"]*/build/#i',
+            '#<link[^>]+href=["\'][^"\']*/build/#i',
             $html,
             'View must not <link> any Vite-built CSS.',
         );
         $this->assertDoesNotMatchRegularExpression(
-            '#<script[^>]+src="[^"]*/build/#i',
+            '#<script[^>]+src=["\'][^"\']*/build/#i',
             $html,
             'View must not <script src> any Vite-built JS.',
         );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$this->assertDoesNotMatchRegularExpression(
'#<link[^>]+href="[^"]*/build/#i',
$html,
'View must not <link> any Vite-built CSS.',
);
$this->assertDoesNotMatchRegularExpression(
'#<script[^>]+src="[^"]*/build/#i',
$html,
'View must not <script src> any Vite-built JS.',
);
$this->assertDoesNotMatchRegularExpression(
'#<link[^>]+href=["\'][^"\']*/build/#i',
$html,
'View must not <link> any Vite-built CSS.',
);
$this->assertDoesNotMatchRegularExpression(
'#<script[^>]+src=["\'][^"\']*/build/#i',
$html,
'View must not <script src> any Vite-built JS.',
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Unit/Framework/Core/ErrorPageTest.php` around lines 58 - 67, The two
assertions in ErrorPageTest.php using assertDoesNotMatchRegularExpression (the
one checking '#<link[^>]+href="[^"]*/build/#i' and the one checking
'#<script[^>]+src="[^"]*/build/#i') only match double-quoted attributes; update
both regexes to accept either single or double quotes around href/src (e.g.
match quote char and use a complementary class for the value) so they also catch
patterns like href='.../build/...' and src='.../build/...'.

Nejcc added 6 commits May 27, 2026 12:23
…ecordings

Builds on e37c2d1 to make the error page genuinely useful instead of
just informative. Page grew 35K → 117K, mostly content; load is still
sub-10ms because everything is server-rendered, inline-CSS, no fonts.

* Ghost: {!! $expr !!} added as Laravel-style raw-output syntax,
  alongside the existing {{{ }}} (which stays for back-compat). Six
  new GhostVars tests lock both shapes + the escaped {{ }} default
  + the @{{ }} skip marker.

* PHP syntax highlighting on every source snippet — driven by native
  token_get_all, tokens wrapped in <span class="t-kw"> etc. Token
  colours inline in the view CSS for both light + prefers-color-scheme
  dark. Multi-line tokens (heredocs, docblocks) span correctly because
  the whole file is tokenised once then sliced by line.

* Suggestions panel — pattern-matched hints for common error classes
  (ParseError → 'check unbalanced brackets', class-not-found → suggest
  composer dump-autoload, PDOException → point at heartbeat + .env,
  NotFoundException → suggest optimize:clear, SQLite path mismatch
  for the literal 'unable to open database file' string, etc.).
  Empty when nothing matches — section just doesn't render.

* 'Copy AI prompt' button — primary purple-tinted button that copies
  a markdown-formatted prompt to the clipboard: error class/message,
  source snippet with the hit line arrow-pointed, app+framework stack
  frames (vendor noise stripped for signal), env block, and a one-line
  ask. Paste-ready for ChatGPT / Claude.
  - Plus secondary 'copy error' + 'copy location' + 'copy trace'.
  - JS uses navigator.clipboard with execCommand fallback for legacy
    browsers / non-HTTPS hosts.
  - All buttons gracefully no-op without JS — page still works.

* 'View full file' disclosure under the source snippet — a <details>
  that expands to show the entire file syntax-highlighted (capped at
  1000 lines for safety). Each line is anchor-targetable as #LN so
  you can deep-link straight to a specific line.

* Recent recordings panel — last 5 requests from
  storage/debug/recordings/ shown as a compact grid (timestamp,
  method, path, status colour-coded by class, duration). Lets you see
  the request just before the failure without bouncing to /debug.

* Visual polish: action-row grouping for copy buttons, primary
  purple-gradient button for the AI prompt, softer dark colours,
  cleaner section spacing, ico spans for emoji-style icons.

* ErrorPageTest assertion tightened to a regex over the real leak
  pattern (link href=/build/ or script src=/build/) instead of a
  string match that false-positived on debug source snippets.

Suite: 131 tests / 283 assertions / 1 pre-existing skip.
UI/UX pass over the advanced error page. Same data, more navigable.

* Sticky TOC at the top — Suggestions / Source / Stack / Request /
  Recent jump links plus a small file-pill on the right showing the
  hit location. Backdrop-blurred translucent background so the
  underlying source stays partially visible while scrolling.

* Every section gets an explicit id (hints, source, stack, request,
  recent) + scroll-margin-top so anchor jumps land below the sticky
  TOC instead of underneath it. html { scroll-behavior: smooth } so
  the jumps animate instead of teleporting.

* Recent recordings rows are now full-width clickable — wrap the row
  contents in an <a href="/debug?tab=recordings&id=…"> with
  display:contents so the layout stays intact. Hover state on rows.

* Section spacing bumped (2.25rem → 2.5rem), h2 .meta given inline-
  flex + explicit gap so the copy-button next to 'Stack trace' sits
  on the baseline cleanly instead of with the ad-hoc margin-left.

Same suite (131 / 283 / 1 skipped, all green); page is 120 KB now.
padding-bottom 1.5 → 2rem and margin-bottom 2 → 3.5rem so the first
section (Suggestions or Source) sits clearly below the dividing line
instead of crowding the header band.
Changed the TOC's margin from `-2rem -1.5rem 0` to
`-2rem -1.5rem 2.5rem` so the header (eyebrow + class title + msg)
sits clearly below the TOC bar instead of butting up against its
bottom border.
Reduce header padding-bottom 2 → 1.5rem and margin-bottom 3.5 → 1.5rem.
With section margin-top of 2.5rem collapsing against it, the gap above
'Suggestions' was stacking to ~3.5rem and looked airy. Now lands
around 2.5rem which matches the rhythm between later sections.
Nine improvements from the self-audit of the new 500 page. All ship
behind no flag; backward-compatible.

REAL FIXES
* Secret redaction in the Request panel. requestContext() walks $_GET +
  Request::all() and replaces values keyed by /password|token|secret|
  api_key|authorization|bearer|session|cookie|csrf|otp|pin/i with
  '[REDACTED]'. Login forms (and similar) no longer leak credentials
  into the rendered HTML.
* First app-kind stack frame auto-opens. normalizeFrames() now sets
  is_first_app on the topmost frame whose source lives under app/ —
  the view honours it as the <details open> attribute. 90% of the
  time it's exactly the frame you want to see first.
* Recording rows no longer link to a fake ?id= query param the
  controller doesn't read. URL is now /debug?tab=recordings#{id} so
  the user lands on the right tab; falling back to a fragment is a
  no-op if the controller doesn't auto-scroll there.
* Single horizontal scrollbar for the source viewer. Lines used to
  scroll independently per cell; .src-lines now overflow-x:auto and
  the line-number cells use position:sticky;left:0 so the gutter
  stays put while the code scrolls under it (IDE-style).

POLISH
* Mobile / narrow rules (<720px): TOC drops the file-pill, header
  stacks vertically, action-row buttons compact, stack-frame
  summaries wrap the file:line under the symbol instead of jamming
  it on the same row.
* Caused-by chain gets a thin red guide line down the left edge,
  with a small dot + connector for each previous exception — makes
  the chain visually obvious instead of a flat list of cards.
* AI prompt now also includes the Request line (METHOD URI / route)
  and a pointer to /heartbeat so the LLM has architectural context,
  not just the immediate failure.

A11Y
* <nav class="toc"> gets aria-label="Error page sections".
* Line-number spans get aria-hidden="true" — purely decorative,
  the line content is the meaning.

SCROLL-SPY
* TOC link of the section currently in view gets .active styling
  (var(--fg) text + accent-coloured underline). Driven by ~15 lines
  of IntersectionObserver; degrades silently if JS is off.

INTENTIONALLY SKIPPED (from the audit's 'skip' tier)
* Search/filter in stack trace, CSS code-splitting, git-blame
  integration. All would add weight or break the
  'page renders when assets are broken' guarantee.

Suite: 131 / 283 / 1 skipped, all green.
@Nejcc Nejcc merged commit ba76bf5 into master May 28, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant