All notable changes to this project are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Vortex\View\Factory::addTemplatePath()— additional Twig filesystem roots (after the application views directory) for package templates.
0.12.0 - 2026-04-05
Vortex\Package\Package— extend and list FQCNs underpackagesinconfig/app.php(read asapp.packages).register()runs right after the config repository is bound (before most core singletons resolve).boot()runs afterroutes/*.phpHTTP routes load (useRoute::for extra endpoints).console()runs while the CLI registers commands (beforeroutes/console.php/routes/*Console.php), suitable forVortex::command(...).ConsoleApplication::boot()loads.envand initializesRepositoryso config-backed CLI behavior and packageconsole()hooks align with HTTP.Package::publicAssets()— map package-relative paths to paths underpublic/;publish:assetscopies files for all configured packages.
0.11.1 - 2026-04-05
Translator: loadslang/{locale}.php(if present) and merges everylang/{locale}/*.phpfile (sorted by path). Fixes apps that only shiplang/en/app.php(dot keys were previously shown verbatim).
0.11.0 - 2026-04-05
- HTTP routes: route files load from
routes/*.php(project root), notapp/Routes/. Files namedconsole.phpor ending in*Console.phpare excluded from HTTP discovery and used for CLI registration only. - Console routes:
routes/console.php(if present) androutes/*Console.phpare loaded forVortex::command(...)(in addition to the previous*Console.php-only pattern under the old directory). AppPathsdefaults:database/migrations,app/Controllers,app/Commands(replacingdb/migrations,app/Http/Controllers,app/Console/Commands).CommandDiscoveryresolves command classes using a PSR-4 namespace derived from the configuredcommandspath underapp/(e.g.App\Commands\). Paths not underapp/cannot be used for auto-discovery namespaces.AppPaths:controllersNamespace()andcommandsNamespace();make:controller,make:command, and stubs follow these namespaces.- Twig: default template root is
resources/views(wasui/views). Override withconfig/view.php→path(orview.pathin the merged config).
Application: readsview.pathwith defaultresources/views.
0.10.0 - 2026-04-04
- Breaking:
app/Routes/*Console.phpfiles are **require**d like HTTP route files; they must callVortex::command(...)instead of returningcallable(ConsoleApplication): void.
CommandDiscovery::registerAppCommands()— registers concreteCommandclasses underAppPathscommands(defaultapp/Console/Commands), recursively.ConsoleApplication::boot()runs it beforeRouteDiscovery::loadConsoleRoutes().AppPaths:commands/commandsDirectory()/commandsRelative()(config/paths.phpkeycommands).Vortex::command()onVortex\Vortex— application facade forapp/Routes/*Console.php(same role asRoutefor HTTP route files).bindConsoleApplication()is@internalforRouteDiscovery.
0.9.0 - 2026-04-04
Command: no constructor argument;ConsoleApplication::register()callssetBasePath()so custom commands use$app->register(new MyCommand()). InvokesetBasePath()yourself when constructing commands outside the console app.
Input:arguments(),argument(),options(),option(),hasOption(),flag()— POSIX-style long/short options,--, whiletokens()remains the rawargv[2..]slice.- Console codegen stubs:
Vortex\Console\Stubrenderssrc/Console/stubs/*.stubwith{{PLACEHOLDER}}substitution.make:command,make:migration,make:model, andmake:controller(invokableApp\Http\Controllers\*, stubcontroller.stub) use stubs.make:modelwritesApp\Models\*(optional--table=,-m/--migrationformigration_model.stubcreate-table migration);AppPathsaddsmodels(defaultapp/Models) andcontrollers(defaultapp/Http/Controllers) alongsidemigrations.
0.8.0 - 2026-04-04
- Packaging: root
README— when to commitcomposer.lock(deployed apps, skeletons) vs omitting it (library package).
- ORM persistence helpers:
Model::refresh()(re-load byid;withTrashedwhen soft deletes),firstOrCreate(),updateOrCreate()(lookup via ANDwhereon the first array). - ORM morph map:
MorphMap::register()(alias →Modelclass),resolveClass(),Model::getMorphClass();morphTo/ eagermorphToresolve aliases;morphMany/morphOneand their eager loads usegetMorphClass()for the_typefilter. FQCNs in the database remain valid when no alias is registered. - Real-time (thin layer):
Broadcasting\Contracts\Broadcaster,SyncBroadcaster(in-processlisten/publish),RedisBroadcaster(broadcasting.driverredis,Redis::publishafter local fan-out;SyncBroadcasteris a separate singleton for listeners);ApplicationregistersBroadcasterandSyncBroadcaster.Response::serverSentEvents(),SseEmitter(message,json,comment), andResponse::isStreamResponse()fortext/event-stream. - Container:
Container::call()invokes callables with auto-wired parameters and named overrides; variadic parameters accept a single array under that argument name. Constructor injection gains union type resolution (first succeedingmake()wins),self/parentnormalization, nullable fallback, and explicitRuntimeExceptionwhen resolving unbound interfaces / abstract classes (instead of a raw PHP error).tag()/tagged()for grouped service lists;bindFor($consumerClass, $abstract, $concrete)for per-consumer (“contextual”) dependency overrides. - JSON Schema (request bodies): dependency
justinrainbow/json-schema;Vortex\Support\JsonSchemaValidator::validateArray();Request::bodyJsonSchemaResponse()returnsvalidation_failedlikebodyShapeResponse(). Empty PHP array[]is treated as JSON{}when the schema root is object-shaped (PHP cannot distinguish them afterjson_decode(..., true)). Draft 3–7 as supported by the library. - JSON Schema (responses):
JsonSchemaValidator::validateDecoded()for any JSON-encodable value (lists and nested structures fromJsonResource::resolve(), etc.).Response::apiOkValidated()/jsonValidated();JsonResource::toValidatedResponse()/collectionValidatedResponse(). Schema mismatch → 500response_schema_mismatchwitherrors(server contract). JsonResourcepipeline: orderedpushResponseTransform()callables (aftertoArray()), thentransformResponse();withResponseTransforms()returns a clone with extra stages.resolve()runs the full chain;toResponse(),toValidatedResponse(), andcollect()useresolve().- Polymorphic ORM:
Model::morphTo(),morphMany(),morphOne();Relation::morphTo(),morphMany(),morphOne()foreagerRelations(). Eager loads batch on{name}_type+{name}_id; nested paths aftermorphTorun per concreteModelclass. - Cursor pagination (API):
Vortex\Pagination\Cursor(encode/decode opaque token),CursorPaginator,InvalidCursorException,QueryBuilder::cursorPaginate()(next_cursor,has_more,per_page;ASC/DESCon a single column).CursorPaginator::toApiData()forResponse::apiOk()payloads. - ORM
hasOne:Model::hasOne(),Relation::hasOne()eager spec, batchedwith()(first related row per parent byidwhen duplicates).hasMany-compatible FK layout on the child. - PSR-16 cache: dependency
psr/simple-cache;Vortex\Cache\Psr16CacheimplementsPsr\SimpleCache\CacheInterfaceoverVortex\Contracts\Cache;SimpleCacheInvalidArgumentExceptionfor illegal keys.ApplicationregistersCacheInterfaceagainst the default cache store. - CLI codegen:
make:migration(timestamped file + anonymousMigrationclass stub) andmake:command(App\Console\Commands\*Commandskeleton + registration hint).replcommand — interactiveeval()with$app,$c,$container; gated byapp.debugor--force.Command::run()stores the booted application once whenshouldBootApplication()is true (no doubleApplication::boot()). - HTTP / routing conventions: abstract
Vortex\Http\Controllerwith smallResponsehelpers; invokable controllers (Route::get('/path', MyController::class)→__invoke);Router::middleware()/Route::middleware()to attach middleware to the route registered immediately before (class names; de-duplicated). - Schema builder:
Blueprintadditions —bigInteger,smallInteger,decimal,floatType,date,dateTime,json,char;ColumnDefinition::unsigned()(MySQL integer family); foreign keyonUpdateactions (cascadeOnUpdate,restrictOnUpdate,nullOnUpdate,noActionOnUpdate).Schema::hasTable()for SQLite / MySQL / PostgreSQL. - Testing:
Vortex\Testing\KernelBrowser—boot(),get()/post()/postJson()/request(),decodeJson(),resetRequestContext()for in-process kernel tests.Container::has()reports explicit bindings or instances. - ORM relation polish:
Vortex\Database\Relation—belongsTo(),hasMany(),belongsToMany()returneagerRelations()spec arrays.Model::load()eager-loads relations on an already-fetched instance (dot paths supported).QueryBuilder::eagerLoadOnto()runs the same batched loader on a list of models using the builder’swith()paths. - JSON body shape validation:
JsonShape::validate()for structural/type checks;JsonShape::object()for nested objects (errorsparent.child);JsonShape::listOf()/listOfObjects()for lists of objects (errorsparent.index.field);JsonShape::listOfPrimitive()for typed primitive lists (errorsparent.index; element spec?typeallowsnullentries).Request::bodyShapeResponse()returns422viavalidationFailed()when the body does not match. Breaking: onlyJsonShape::object([...])defines nesting; raw associative arrays as specs are rejected. Not JSON Schema; supports optional fields (?type) and typesstring,int,float,bool,number,array,list,object. - REST resource routing:
Router::resource()/Route::resource()— registersindex,store,show,update,destroy; defaultexceptcreate/edit. Named routes (photos.index, …); optional name prefix; sharedmiddleware.parameteroverrides singular placeholder (e.g.categories→{category}). - HTTP JSON API helpers:
Response::apiOk()/Response::apiError()for stable success and error envelopes; abstractJsonResourcewithtoArray(),toResponse(),collect(), andcollectionResponse();Response::validationFailed()mapsValidationResultto422/validation_failedpluserrors.Request::validationResponse()/bodyValidationResponse()runValidator::makeand return that response when invalid.Request::splitVersionedPath(),apiVersionFromHeaders(),resolvedApiVersion(),matchesApiVersion(),withPath()for/v1/...and header-based versions.ErrorRendererJSONnotFoundusesResponse::notFound(); 500 JSON usesapiError(). - ORM eager loading:
Model::eagerRelations()maps relation method names tobelongsTo,hasMany, orbelongsToManyspecs soQueryBuilder::with()batches related queries. Nested relations use dot paths (e.g.author.country). Without a map entry,with()still resolves by calling the relation method on each model. Invalid spec entries throwInvalidArgumentException. - Route model binding:
Router::model($parameter, $modelClass, $column = 'id')andRouter::bind($parameter, Closure $resolver);Route::model/Route::binddelegate to the active router. Resolvers run before the action; missing model or resolver returningnullyieldsErrorRenderer::notFound()(404). Model class must extendModel. - Model global scopes:
Model::addGlobalScope()registers named callbacks applied when buildingquery();QueryBuilder::withoutGlobalScope()/withoutGlobalScopes();all()andfind()usequery()so scopes apply (breaking if you relied on unscoped direct SQL). - Model soft deletes:
$softDeletes,$deletedAtColumn;find()/all()/QueryBuilderexclude trashed by default;withTrashed(),onlyTrashed(); instancedelete()/restore()/forceDelete(); mass soft delete via querydelete();onlyTrashed()->delete()hard-deletes.updateRecord()addsdeleted_at IS NULLwhen soft deletes are enabled. - Model casts:
protected static array $castsonModel(int,float,bool,string,json/array,datetime); applied infromRow()and when persisting viagatherFillableFromInstance()/updateRecord(). - Model observers:
Model::observe()registers handlers per model class; lifecycle hookssaving,creating,updating,deleting,saved,created,updated,deletedrun aroundcreate(),save(), anddelete().Model::forgetRegisteredObservers()for tests. Breaking / behavior:create()and new-recordsave()shareperformInsert(same events); when$fillableis empty, attributes are taken from all public/dynamic instance properties for persistence (omit unsetidon insert). - Queue:
Vortex\Queue\Contracts\QueueDriver;Vortex\Queue\RedisQueuewhenqueue.driverisredis(ready list + delayed ZSET + JSON envelope; configurequeue.redis); sharedVortex\Support\PhpRedisConnectfor phpredis. Breaking:DatabaseQueue::delete/::releasetakeReservedJob;ReservedJobincludes the queue name for Redis retries. - Cache:
redisdriver inconfig/cache.phpstoresmap (RedisCache, phpredis viaPhpRedisConnect); requires ext-redis. Values are PHP-serialized;clear()usesSCAN+DELfor the key prefix.memcacheddriver (MemcachedCache,PhpMemcachedConnect); requires ext-memcached;clear()bumps a generation token (no pool-wide flush).add($key, $value, $ttlSeconds): boolonVortex\Contracts\Cache(NX;RedisCacheusesSETNXEX;NullCache::addalways returns true).Cache::add()on the default store facade. - Schedule:
Vortex\Schedule\Scheduleloadsconfig/schedule.php(taskswithcron+class), supportsSchedule::register()duringApplication::boot(),CronExpression::isDue()(five fields:*, integer,*/step, comma lists, hyphen ranges), optionalwithout_overlapping/mutex_ttl(mutex_ttl_seconds) on tasks andmutex_store(named cache store) in schedule config for overlap guards viaCache::add(), CLIschedule:run, andapp.timezonefor “now”.Repository::initialized()for safe reads when the repository was not booted. - Queue:
Vortex\Queue\Contracts\Job,DatabaseQueue(SQL table + reservation / stale reclaim), staticVortex\Queue\Queue::push(),DatabaseQueue::pushSerialized(), CLIqueue:work,queue:failed,queue:retry, andFailedJobStore(permanent failures recorded whenqueue.failed_jobs_tableis set; empty string disables recording). Config:queue.table,queue.default,queue.tries,queue.stale_reserve_seconds,queue.idle_sleep_ms,queue.failed_jobs_table. Vortex\Auth\Authsession facade:loginUsingId,login(Authenticatable),logout,check,guest,id,userwith optionalresolveUserUsingcallback;login/loginUsingIdaccept$rememberto set a signed remember cookie;logoutclears the remember cookie.Vortex\Auth\Gate—define(),policy(),allows(),denies(),authorize();AuthorizationExceptionfor deniedauthorize()(rendered as 403 byErrorRendererwithout the full exception log path).Vortex\Auth\RememberCookie— signed payload (requiresAPP_KEY);Vortex\Auth\Middleware\RememberFromCookierestores session from the cookie.Vortex\Auth\PasswordResetBroker— hashed single-use tokens in SQL with configurable table name and TTL;issueToken,tokenValid,verifyAndConsume,purgeExpired.- Middleware:
Vortex\Auth\Middleware\Authenticate(JSON 401 or redirect toauth.login_path); abstractAuthorizeAbility(403 whenGate::denies). Vortex\Auth\AuthConfigreadsauth.login_path,auth.remember_cookie,auth.remember_seconds,auth.cookie_secure,auth.cookie_samesitewhen the config repository is available.- Twig
gate_allows(optional second argument for policy context; one-argument calls delegate toGate::allows($ability)only).
- Breaking: When
Request::wantsJson(),Response::error()(includingnotFound(),forbidden(),unauthorized()) JSON now always includeserror(machine-readable code; shortcuts set values such asnot_found, defaulthttp_error) withokandmessage.
0.7.0 - 2026-04-03
- Fluent validation rule builder
Vortex\Validation\Rulewith inline per-rule message support;Validator::make()now accepts rule objects. - Fluent response/session flash helpers:
Response::with(),withMany(),withErrors(),withInput(), and session batch helpersSession::flashMany()/flashPutMany(). - Request-aware response shortcuts:
Response::error(),notFound(),forbidden(), andunauthorized()that auto-select HTML or JSON output.
Request::wantsJson()now treatsX-Requested-With: XMLHttpRequestas JSON-preferring requests.- Breaking:
composer.lockis no longer tracked in the repository and is now ignored.
0.6.0 - 2026-04-03
- Model relation helpers in
Vortex\Database\Model:belongsTo,hasMany, andbelongsToMany. - Query builder feature expansion in
Vortex\Database\QueryBuilder:select,with(eager loading), joins, grouped/or where clauses,pluck,value, bulkupdate,delete, and raw result methods. - Configurable Twig extension registration via
app.twig_extensionsin app config; extensions are injected throughVortex\View\Factory. - Twig function
benchmark_ms()for reading named benchmark timings in views.
- Breaking: Migration classes now extend abstract
Vortex\Database\Schema\Migrationand implement parameterlessup()/down()methods. - Breaking: Migration IDs are now resolved from migration filenames instead of
Migration::id(). - Schema builder now exposes static entrypoints (
Schema::create,Schema::table,Schema::dropIfExists) with connection scoping handled by the migrator. - Console command execution flow is unified under the base
Vortex\Console\Commandlifecycle. - Default model table-name resolution now pluralizes snake_case model names; explicit
protected static ?string $tableoverrides are supported.
0.5.0 - 2026-04-03
- Optional
config/paths.php— configuremigrations(migration class directory) relative to the project root.Vortex\Support\AppPathsresolves it and CLI commands use the same rules.
- Breaking: Console commands (
migrate,migrate:down,db-check) now boot the app container directly viaApplication::boot(); startup container files are no longer required.
0.4.0 - 2026-04-03
- Breaking: Migration PHP classes are loaded from
db/migrations/(wasdatabase/migrations/). - Breaking: Console commands that load the app container (
migrate,migrate:down,db-check) requirestartup/app.php(wasbootstrap/app.php). - PHPDoc and uninitialized-facade errors now refer to
Application::boot()instead of the word “bootstrap” where that meant the app entrypoint.
0.3.0 - 2026-04-03
- Class-based database migrations —
database/migrations/*.phpclasses withid(),up(), anddown();SchemaMigratorandVortex\Database\Schema(Schema,Blueprint,ColumnDefinition,Migration);php vortex migrateandmigrate:down(rollback last batch); state invortex_migrations. Database\Schema\Schemafluent builder with Laravel-like columns (id,string,text,integer,boolean,timestamp,timestamps,foreignId,index,unique).mockery/mockeryas a dev dependency;MockeryIntegrationTestexercises container wiring with mocks.Application::boot()loads.envviaEnv::load, registersCsrf,LocalPublicStorage,Translator, andErrorRenderer, sharesappNameinto Twig, and accepts an optional?callable $configure(Container, string $basePath)after defaults (before route discovery).
- Breaking: Database migrations are class-based (see Added). Older ad-hoc migration formats are not supported by
migrate.
0.2.0 - 2026-04-03
php vortex doctor— whenconfig/files.phpexists, checks each upload profile’sdirectory: exists underpublic/, writable, create/delete probe.FilesConfigUploadRootsparses the config shape.Log::setBasePath()at bootstrap;Log::info,warning,error,debug,notice,critical,alert,emergency,log($level, …)with optional JSON$context; samestorage/logs/app.logas exceptions.Cookievalue object (Set-CookieviaResponse::cookie()orCookie::queue()+Cookie::flushQueued()inKernel/Application::run()),Request::cookie()/cookies(),Cookie::parseRequestHeader()(quoted values,SameSitehelper shared withSession).Files\Storagefaçade:disk($name)returnsFilesystemdrivers fromconfig/storage.php(local,local_public,null); default disk forput/get/…;storeUpload/publicRootuseupload_disk/public_disk.Storage::setBasePath($basePath)at bootstrap (Application::bootand app bootstrap).Support\Benchmarkstatic stopwatch helper with named timers:start,has,elapsedNs,elapsedMs,elapsedSeconds,measure,forget.
- Breaking: Database is multi-connection:
config/database.phpusesdefaultandconnections.{name}.driver(sqlite, mysql, pgsql).Vortex\Database\DatabaseManageris registered in the container;Connectionis constructed with aPDOfrom the manager;DB::connection(?string)selects a connection. Env:DB_CONNECTION(default connection name, defaultdefault). - Breaking: Cache is multi-store:
config/cache.phpusesdefaultandstores.{name}.driver(like storage disks).Vortex\Cache\CacheManageris registered in the container;Cache::store(?string)selects a store;CacheFactory::make()returns the default store via the manager. Env:CACHE_STORE(default store name),CACHE_DRIVERonly used whenCACHE_STOREis unset. - Breaking: Session is multi-store:
config/session.phpusesdefaultandstores.{name}.driver(native,null).Vortex\Http\SessionManageris registered in the container;Sessionfacade uses the default store;Session::store(?string)selects a store;Csrfnow reads/writes through theSessionfacade. Env:SESSION_STOREcontrols default store name. - Breaking:
QueryBuilder::paginate()returnsVortex\Pagination\Paginatorinstead of an array. Use$paginator->itemsand the same public count fields (total,page,per_page,last_page). For page links, callwithBasePath()(e.g. withroute('name')) thenurlForPage($n); helpershasPages(),onFirstPage(), andonLastPage()are available for templates. - Breaking:
Log::exception(Throwable $e)only — project root comes fromLog::setBasePath()(Application::boot()and app bootstrap call it).ErrorRendererhas no constructor parameters.
0.1.0 - 2026-04-03
- HTTP route files (
app/Routes/*.phpexcept*Console.php) are required at discovery time and must register routes at the top level withRoute::get/post/add— noreturn static function (): void { … }wrapper. Console route files returnedcallable(ConsoleApplication): void(later:Vortex::command(), see Unreleased).
- Cache-backed rate limiting and stricter doctor —
RateLimiter,Middleware\Throttle(fixed window);php vortex doctorrequiresext-mbstring, and production checks require non-emptyAPP_KEY. - Named routes and URL generation —
Router::name,Router::path,Route::name(), globalroute(), Twigroute(),Router::interpolatePattern(). - In-process HTTP handling and route loading —
Kernel::handle(Request),Request::make()/normalizePath(),Response::headers()for tests; HTTP route files are loaded viarequire(see Breaking). Fixture app andKernelHandleTestin the framework test suite.
Kernel::send()appliesTrustProxies, builds the request, delegates tohandle(), then sends the response.
0.0.1 - 2026-04-03
- Initial public release of Vortex (
vortexphp/framework). - Application core:
Application,Container,AppContextfor bootstrapping and dependency injection. - HTTP:
Kernel,Request,Response,Session,Csrf,TrustProxies,ErrorRenderer,UploadedFile. - Routing:
Router,Route,RouteDiscovery. - Database:
Connection,DB,Model,QueryBuilder(PDO). - Views: Twig integration via
View,Factory, andAppTwigExtension. - Mail:
MailFactory,SmtpMailer,NativeMailer,NullMailer,LogMailer,MailMessage, encoding helpers. - Cache:
Cachecontract,FileCache,NullCache,CacheFactory. - Config:
Repositoryfor configuration access. - Console:
ConsoleApplication,ServeCommand,SmokeCommand,DoctorCommand,DbCheckCommand,MigrateCommand. - Events:
EventBus,Dispatcher,DispatcherFactory. - I18n:
Translatorandhelpers.phpautoloaded helpers. - Validation:
Validator,ValidationResult. - Crypto:
Password,Crypt,SecurityHelp. - Support:
Env,Log, and string/array/URL/JSON/HTML/date/number/path helpers. - Files:
LocalPublicStoragefor public disk paths. - Contracts:
Cache,Mailer,Middleware. - PHPUnit test suite under
tests/.