Extract System/ into a standalone Composer package silverengine/core living in
Packages/silverengine/core/, the same way silverengine/error-handler already works.
Modernise every migrated file to PHP 8.4+ idioms as it moves.
Strategy: incremental migration — move files to package, modernize to PHP 8.4+,
delete from System/. Unmigrated files stay in System/ via root PSR-4 fallback.
Framework stays bootable at every step.
Dependencies added: vlucas/phpdotenv, nejcc/php-datatypes
Naming: Blueprints renamed to Contracts, MigrationCore to MigrationInterface
- Create
Packages/silverengine/core/composer.json(php >=8.4, vlucas/phpdotenv) - Create
Packages/silverengine/core/src/directory tree - Add
silverengine/core+nejcc/php-datatypesas deps in rootcomposer.json - Run
composer update— autoload resolves from package - Root
Silver\\PSR-4 kept as fallback for unmigrated files
-
Core/Env.php— rewritten for vlucas/phpdotenv, typed properties -
Core/Config.php— typed returns -
Core/App.php— typed properties, implementsInstanceInterface -
Core/Instances.php— fully typed container -
Core/DI.php—ReflectionNamedType(replaces deprecatedgetClass()) -
Core/Kernel.php—neverreturn, arrow fn middleware chain -
Core/Route.php—str_starts_with/str_ends_with, throw expressions -
Core/Controller.php— declared$controllerName, fixed types -
Core/Model.php— declared all subclass properties, removed unused -
Core/Library.php— typed params,neverreturn ondd() -
Core/ErrorHandler.php— handles\Throwable,neveronfinalize() -
Core/Bootstrap.php—readonlyproperty -
Core/AppInstanceTrait.php—staticreturn type -
Core/helpers.php—env()helper withmatchexpression - Old
System/Core/files deleted, package loads confirmed -
.envfile created (replaceslocal.env.phpapproach) -
public/index.phprewritten (dotenv, first-class callables for error handlers) -
silverCLI entry point modernized
-
Core/Contracts/InstanceInterface.php—staticreturn type -
Core/Contracts/MiddlewareInterface.php—: mixedreturn -
Core/Contracts/RenderInterface.php—: string,: arrayreturns -
Core/Contracts/Http/RequestInterface.php— typed returns -
Core/Contracts/Http/ResponseInterface.php -
Core/Contracts/Database/MigrationInterface.php— renamed fromMigrationCore - All
System/andApp/implementors updated to useContractsnamespace - Old
System/Core/Blueprints/deleted
-
Core/Bootstrap/Autoload.php—str_starts_with() -
Core/Bootstrap/ServiceProvider.php— typed params -
Core/Bootstrap/Facades/Request.php—final, typed return -
Core/Bootstrap/Facades/Response.php—final, typed return -
Core/Bootstrap/Facades/Log.php—final, typed return -
Core/Bootstrap/Facades/FakeFactory.php—final, typed return -
Core/Storage/Cache.php—final, typed properties, fixed$time_or_predictorbug -
Core/Http/Lang.php—final, null-safe
-
Exception/Exception.php—?\Throwableprevious param -
Exception/ErrorException.php—final -
Exception/NotFoundException.php—final
Env, Config, DI, Instances, Bootstrap, ErrorHandler, Cache, Lang, all 4 Facades, ErrorException, NotFoundException
-
Http/Request.php— typed properties,str_starts_with,?Routereturn -
Http/Response.php— nestedmatchfor content dispatch,RenderInterface -
Http/Session.php—final, removed side-effect auto-call -
Http/Cookie.php—final,matchfor return types -
Http/Curl.php—final, removed deprecatedcurl_close() -
Http/Redirect.php—final,neverreturn types -
Http/Validator.php—final,string|falsereturn types -
Http/View.php— DRY template extension loop,str_starts_with
- 55 files migrated: Db, Query, Model, Compiler, QueryObject, Relation, Source, DBCreator
- Parts/* (20 files), Query/* (10 files), Traits/* (8 files), Source/* (3 files)
- DB-specific variants: Mysql/, Pgsql/, Sqlite/
-
declare(strict_types=1)added to all files - Fixed
$self::isDebug()typo in Db.php - SQLite connection verified
-
Support/Facade.php—abstractbase, lazy singleton??= -
Support/Fake.php—final, DRY via__callStatic+ const array -
Support/FakeFactory.php—const arraydata,random_int() -
Support/Log.php—final, typedconst array TYPES -
Support/Crypter.php—final,matchfor alphabet,random_int() -
Support/Git.php—final,readonlybranch property -
Support/SMail.php—final, typed properties, fluent builder -
Helpers/Str.php— renamed fromString(reserved word), wraps builtins -
Helpers/Path.php—final,str_starts_with -
Helpers/URL.php—str_starts_with/str_ends_with -
Helpers/HTMLElement.php— typed constructor, union types
-
Engine/CLI.php—matchexpressions,neverreturn, DRYresolvePaths -
Engine/Events/EventManager.php— fixed namespace, typed, removed debug echo -
Engine/Ghost/Template.php— DRYprocessLines()helper, arrow fns
-
App/Controllers/SystemController.php— typed return -
App/Controllers/MigrationsController.php—string|falseunion type -
App/Middlewares/*— allfinal,MiddlewareInterface,\Throwablecatch -
App/Routes.php— strict comparison -
App/Views/*— copied to package - Config/Routes.php updated to point to package path
-
System/directory deleted entirely -
"Silver\\": "System/"removed from rootcomposer.json -
Config/Providers.php— removed staleSilver => Systemmapping -
Config/Routes.php— updated system routes path - Clean boot verified (zero warnings)
- Run full test suite (
Tests/) — PHPUnit ^12 added as require-dev (dev-only, runtime stays dependency-light). Suite green: 14 tests, 19 assertions, 1 skipped (network-only Curl test). -
Update— resolved by removal: package absorbed intoerror-handlertophp >=8.4silverengine/core(Silver\ErrorHandler\Reporter), second path repo dropped - Tag
silverengine/corev0.1.0 — intentionally deferred (tag step skipped by request; code is tag-ready)
- Shared view data — unified
View::share()store, available in Ghost views AND Wisp pages (Wisp::share()delegates to it) - View composers —
View::composer($pattern, $cb), exact + fnmatch wildcard, merged at render viaView::sharedFor() - Request improvements — typed
headerValue(),hasHeader(),query(),json(),bool(),int(),wantsJson(); WispResponse + Wisp middleware refactored off raw$_SERVER
- Vite + Vue 3 + TypeScript + Tailwind 4, official
@inertiajs/vue3client - First-party
Silver\Engine\Ghost\{Wisp,WispResponse,Vite,LazyProp,DeferProp},{{ wisp() }}/{{ vite() }}directives,wisp()helper - Inertia wire protocol (X-Inertia headers), version 409 / 303 handshake middleware
- Lazy + deferred props, partial reloads, prefetch-ready
deferredProps -
composer devruns PHP + Vite concurrently;composer serve= PHP only
-
declare(strict_types=1); - Typed / readonly properties
- Constructor promotion
-
matchinstead ofswitch -
str_starts_with/str_ends_with/str_contains - Union / intersection / nullable types
-
finalon leaf classes - First-class callable syntax (
$this->method(...)) -
neverreturn type - Enums for finite value sets —
Silver\Http\HttpMethod,Silver\Database\DbDriver,Silver\Support\LogType,Silver\Support\PasswordCharset -
array_find,array_any,array_all(PHP 8.4+) —Response::send()(array_find),View::sharedFor()(array_any); remaining loops are accumulation/transform, not idiomatic candidates - [~] Property hooks where they simplify getters/setters — no
behaviour-preserving application in core: every get/set is
public method API (converting breaks callers), and the only magic
accessors (
QueryObject) are a dynamic property bag hooks can't model. Deferred to Phase B (where API changes are in scope). - Remove remaining legacy phpdoc that duplicates native types — Db/Query/Model/DBCreator stripped of zero-info tags; provably-safe native return types added; informative phpdoc kept
Mechanical-first, behaviour-preserving. PHPUnit ^12 dev-only baseline. 5 commits. Flagged for follow-up (Phase B / separate fix):
Database/DBCreator.php— dead code, zero references; candidate for deletion— fixed (ColumnDef::compileReference()ON UPDATEspacing bugfix(db)); referential actions still an enum candidate for Phase B- Ambiguous return types left untyped:
Db::{toSql,isDebug,quote,commit, transaction,driverName,fetch},QueryObject::__get/__set
Test-first: characterization tests pin observable behaviour BEFORE each structural change. Per-batch gate (tests → refactor → suite green → commit). PHPUnit baseline grew 8 → 35 tests, 62 assertions, 1 skipped.
-
B6 — drop legacy
$_member prefix.Routeprivate/static members$_foo → $foo($jails/static→$jailStack; PHP forbids same-name instance+static).QueryObject::$_table/$_primary → $table/$primaryKey(typed?string, keptstatic— static active-record needs table/PK without an instance); migratedApp/Models/Users.php. Tests:RouteTest,QueryObjectTest. -
B1 — dialect Strategy. Extracted
Silver\Database\Dialect(segment()/classFor()) from the inlineCompiler::toSql()ucfirst+substr_replace+class_exists; usesDbDriverenum, identical fallback. Tests:DialectCompileTest(SQL pinned pre-refactor),DialectTest. -
B2 — Query factory +
QueryTypeenum. Replaced stringlyQuery::instance();queryClass()/make()resolve the identical FQN. Test:QueryTypeTest(enum map + insert/update/delete/drop SQL pinned). -
B3 — CLI
Commandenum. Replacedmatch($this->cmd)literals; alias-awareparse()(c→Generate, unknown→null). Proportionate — no class-per-command framework. Test:CommandTest. -
B4 — split the
DbGod class. ExtractedConnectionManager(registry/lazy-PDO/raw/exec/quote/lastInsertId/driverName) andTransactionManager(depth counter + nested BEGIN/COMMIT/ROLLBACK + SAVEPOINT levels, savepoint SQL still viaDb::exec()for the debug echo).Dbis now a thin BC facade keeping only the per-instance fetch/debug side; everyDb::/Query::/Model::static entry point unchanged. Found + fixed a pre-existing bug first (separatefix(db)):class_exists($style)before a string check threwTypeErrorunderstrict_types— all builder result-fetch with default style was broken. Tests:DbBehaviorTest(10, characterization pinned pre-refactor),DbFetchTest(5, TDD red→green for the bug). Ambiguous returns (Db::{toSql,isDebug,quote,commit,transaction,driverName,fetch}) left untyped — separate optional polish, see backlog.
- B5 — real IoC container. Today
Instancesis a registry, not a container. Addbind()/singleton()/ interface→impl / closure factories / recursive autowiring + constructor injection for controllers & middleware. Plan: (1) characterization of the current shallowDI::callcontract (method injection by class name + route vars); (2) introduceContainer(autowiring + bindings) keepingDI::callsemantics as the method-injection front end; (3) constructor-inject controllers inKernel::findCallableandnew $mw()inloadMiddlewares; (4) unify the 3 singleton mechanisms; routeFacadethrough the container; (5) fix theServiceProvidercontract.
Database/DBCreator.php— dead code, zero refs; delete candidate.Builder fetch— fixed (class_exists(int)TypeErrorfix(db), TDD,DbFetchTest).- Ambiguous return types still untyped (optional polish, low value):
Db::{toSql,isDebug,quote,commit,transaction,driverName,fetch},QueryObject::__get/__set(dynamic property bag — not a property-hook candidate). Route::url()depends on the globalBASEPATHconstant defined only bypublic/index.php— should come via config/container (B5). Test env definesBASEPATH=''.- Dual
Modellineage:Silver\Database\Model(static active- record, extendsQueryObject) vsSilver\Core\Model(instance,protected string $primaryKey). Reconcile/clarify in B4. ServiceProviderinterface (before(mixed $kernel)/register(mixed $app)/after()) does not match howKernelinvokes providers (before($req,$res)/after($req,$res),register()never called). Fix in B5.- Dead
Kernel::call()static (duplicatesDI, no callers) — remove in B5. Facadekeeps its ownstatic $objectscache outsideInstances— same class can exist twice. Unify in B5.- Cosmetic:
Query\DeletecompilesDELETE FROM(double space) — valid SQL, left verbatim (behaviour-preserving); tidy opportunistically.
Profiled with the in-repo DebugTimer/RequestRecorder over 50 recorded
requests (/, /wisp-demo, /demo) on the php -S dev server.
| phase | dev server | warm process |
|---|---|---|
Env::construct |
6.32 | 0.77 |
| view render | 1.53 | — |
| per middleware (×5) | ~1.2–1.5 | — |
| database connect | 1.38 | — |
| controller resolve | 0.74 | — |
| controller action | 0.38 | — |
| route resolve | 0.014 | — |
| services | 0.008 | — |
| boot header (autoload→mw) | 21.65 | — |
- The apparent
Env::constructhot spot is a no-opcache artifact: this runtime has no opcache at all, sophp -Srecompiles every file every request. Warm (single long-lived process, mimicking php-fpm+opcache)Env::constructis 0.77 ms, and thejson_decode(json_encode())config clone I suspected is 0.035 ms — hypothesis rejected by measurement. - No code path is algorithmically slow. Route match 0.014 ms, services 0.008 ms, controller action 0.38 ms. Speculative code micro-optimisation would add risk for zero proven gain — explicitly against measure-first.
composer.json→config.optimize-autoloader: true(+ sort-packages): everycomposer installnow emits a classmap with PSR-4 fallback. Dev boot 21.65 → 20.62 ms; the real benefit is production scale (no per-class PSR-4 stat walk).
- opcache is the dominant production lever. With php-fpm + opcache the boot collapses toward the warm numbers (~3–5 ms total vs ~21 ms). No code change can match this; it is a deploy requirement.
- Do NOT use
classmap-authoritativewith this framework. It would break correctness:Kernel::findCallable()resolves App controllers by file convention (include_once+new $class), andDialect::classFor()/QueryTyperely onclass_exists()for optional dialect/query variants — an authoritative classmap returns false for anything not pre-listed (user controllers, dialect variants), breaking dynamic resolution.optimize-autoloaderis the correct, safe choice (classmap + PSR-4 fallback).
All phases (A, B, C) complete. Remaining items live in the findings backlog above (dead
DBCreator, optionalDbfacade return-typing).