Upgrading
This guide lists only what you have to do when you move to a newer glueful/framework: breaking
changes, removed APIs and config, required migrations, and subsystems that moved to their own
packages. For the full story of each release, see Release Notes (v1.70 and later) and the
Release Archive (v1.69 and earlier).
How to upgrade
- Update the package.
composer update glueful/framework - Read every section between your current version and the target. Sections are newest first; versions with nothing to do are left out. Jumping several versions means applying each section on the way.
- Run the deploy steps.
php glueful migrate:run # apply any new core migrations php glueful route:cache:clear # drop a route cache compiled by the old version
You don't needphp glueful commands:cacheon 1.87 or later: the production command manifest is stamped with the framework version that wrote it and rebuilds itself on the first boot after an upgrade. Run it only if you want to warm the manifest as part of your deploy.cache:clearnever touches the command manifest. - Compare your
config/copies with the framework's. Since 1.86, a list your app's config sets (aconfig/schedule.phpjobslist, above all) replaces the framework's list instead of being merged into it. New framework entries — scheduled jobs, CSRF exempt routes and so on — don't reach your app until you add them to your own copy.
Leaving a removed config key in your own copy changes nothing — it is simply no longer read. The removals below are listed so you can tidy up, not because a stale key breaks anything.
Upgrading to 1.91
1.91 makes a large group of settings and attributes that were silently ignored actually take effect. Most items below are behaviour that now matches what you wrote — review each one, because a value that used to do nothing now applies.
Run php glueful route:cache:clear on deploy. The route cache format moved to v3 (old files are
also invalidated on the next boot). Routes served from the old cache lost their rate limits,
#[Fields] and #[RequireScope] config — scoped routes served from the cache let requests through.
Security & auth
- Stacked
#[RequiresRole]attributes mean "any one of these roles". They were enforced as "all of them". Roles from the method, its class and its parent classes form one set, so a method-level role widens a class-level one. If a route really needs several roles at once, use#[RequiresPermission](stacked permissions are still all required) or a single combined role.#[RequiresRole('admin')] #[RequiresRole('editor')] public function publish(): Response { } // 1.91: admin OR editor (was: admin AND editor) - CSRF config is applied. The
csrfmiddleware was built from constructor defaults, soconfig/security.php'scsrfblock was ignored. With the shipped config:- tokens expire after 3600s (was 7200s) —
CSRF_TOKEN_LIFETIME; - the
exemptRouteslist (auth/login,auth/register,webhooks/inbound/*,public/*,csrf-token, …) now skips CSRF; CSRF_PROTECTION_ENABLEDandCSRF_DOUBLE_SUBMITtake effect.
Review your app'ssecurity.csrfblock before upgrading. If your app has its ownconfig/security.php, itsexemptRouteslist replaces the framework's. - tokens expire after 3600s (was 7200s) —
exemptRoutesentries match under the API prefix. Short entries such asauth/loginnow match/api/v1/auth/login(full-path entries keep working). Check that no route you meant to protect now matches a shipped exemption.- The shipped webhook exemption is
webhooks/inbound/*(it waswebhooks/*, which never took effect before 1.91 and would have exempted the authenticated management API at/api/v1/webhooks/...). Mount inbound third-party receivers under/webhooks/inbound/<provider>, or give them their ownexemptRoutesentry. If your app has its ownconfig/security.php, changewebhooks/*there too. - Cookie-transport writes with no
Origin/Refererget403 CSRF_ORIGIN_MISMATCH.skip_for_bearer_authused to skip the origin check because theAuthorizationheader had been injected bysession_cookie. Browsers sendOriginon cross-origin writes; non-browser clients using the session cookie must send one too. - A split-subdomain SPA needs an exact CORS origin with credentials for
/auth/session/refreshand/auth/session/logoutto acceptSec-Fetch-Site: same-site:CORS_ALLOWED_ORIGINS=https://app.example.com CORS_SUPPORTS_CREDENTIALS=true
A wildcard (*) origin never qualifies; an exact entry (or an anchoredCORS_ALLOWED_ORIGIN_PATTERNSmatch) with credentials on does. - CORS origin patterns apply to real responses, not just preflight. Origins matching
CORS_ALLOWED_ORIGIN_PATTERNS(and every origin whendevelopment_allow_allis on in development) now getAccess-Control-Allow-Originon actual responses. Patterns are comma-separated PCRE regexes with delimiters; make sure each is anchored. See CORS & CSRF.# Unanchored — also matches https://app.example.com.attacker.net CORS_ALLOWED_ORIGIN_PATTERNS=/https:\/\/.*\.example\.com/ # Anchored CORS_ALLOWED_ORIGIN_PATTERNS=/^https:\/\/[a-z0-9-]+\.example\.com$/ - JWT requests can pass
#[RequireScope]routes. Scopes come from the token'sscopesclaim, ascopearray or space-delimitedscopestring ("read:posts write:posts"), the user record'sscopes, or anidentity.claims_provider. A JWT with no scopes is still denied — unlike an API key, empty does not mean unrestricted. If you minted JWTs with scope claims, those users may now reach routes they were refused before. - Clear
SESSION_COOKIE_REFRESH_PATH=/auth/sessionfrom existing.envfiles so the derived path (api_prefix() . '/auth/session', e.g./api/v1/auth/session) applies. Keep an explicit value only if your session routes really are served there. See Browser Sessions. - Gate-only apps:
requirePermission()consults the Gate. With no permission provider installed, endpoints that call it (metrics, health, config, …) refused everyone with "Permission system unavailable"; they now admit users whose Gate roles or voters allow it. Check the voters in yourconfig/permissions.phpbefore upgrading. - Signed blob URLs issued earlier point at an unrouted path (
/blobs/{uuid}without the API prefix). Generate new ones.
Routing & middleware
->rateLimit()and#[RateLimit]are enforced. Therate_limitmiddleware is now attached automatically (never duplicated if you addedrate_limit,rate_limit:…orenhanced_rate_limityourself), so these routes can return429with rate-limit headers. See Rate Limiting.- A class-level
#[RateLimit]applies to every route in the controller that has no method-level#[RateLimit]of its own (a method-level one replaces the class set). Check these limits before upgrading. rate_limit:N,W,user|endpointkeys by user or endpoint instead of always by IP. Unknown values fall back toip.#[Fields]routes project their responses by?fields=/?expand=(thefield_selectionmiddleware is attached automatically) and return400for strict whitelist violations.useAdvancedPatterns: truenow makesallowedact as patterns (posts.*,-password). See Field Selection.csrf:andrequest_logging:route params take effect. Values written in a route definition were ignored and the middleware ran on defaults:$router->post('/forms/contact', [FormController::class, 'store']) ->middleware(['csrf:3600,false,false']); // lifetime, double-submit, validate-origin — now honoured $router->get('/reports', [ReportController::class, 'index']) ->middleware(['request_logging:both,true,true']); // now honoured
Review routes that pass these params: the values you wrote now apply.- The container-built
tracingmiddleware followsTRACING_ENABLED(defaultfalse) and passes requests straight through when disabled. SetTRACING_ENABLED=trueif you bind a real tracer. API_RATE_LIMITING_ENABLED=falseandapi.field_selection.enabled = falseswitch those features off. Neither key was read, so rate limits and field selection always applied. Check apps that set them tofalse.- OpenAPI tags skip the API prefix. Under
/api/v1every operation was tagged "Api"; tags now come from the first segment after the prefix (e.g. "Users"). Generated SDKs and docs grouping change — regenerate them withphp glueful generate:openapi. See OpenAPI.
Validation & database
unique:andexists:rules are enforced. They silently passed before, so requests that used to pass may now get422. The rules fail closed: with no connection or context they throwLogicException. AValidatoryou build by hand needs a context or connection:// Before — unique:/exists: silently passed $validator = new Validator($rules); // After $validator = new Validator($rules, $context); // or $validator = new Validator($rules, connection: $connection);
(Or callsetConnection()on the rule.) Database-backed custom rules can implement the newGlueful\Validation\Contracts\DatabaseAwareRuleto receive the Validator's connection. See Validation.nullableandsometimestake effect. A null or empty value stops the rules afternullable; an absentsometimesfield skips all its rules and no longer appears infiltered()/validated().useCurrentOnUpdate()throwsUnsupportedSchemaOperationExceptionon PostgreSQL and SQLite instead of being silently ignored (MySQL now emitsON UPDATE CURRENT_TIMESTAMP). Use a trigger or set the column in application code, and gate the call by driver in portable migrations.ChunkedDatabaseProcessor's constructor is typedConnection|PDO|mysqli. Pass a GluefulConnection(now supported) or a raw driver handle.- SQLite unique violations return
409, not500. Adjust tests that expected a 500. - Deprecated:
QueryBuilder::optimize()— it never had an effect and is now a no-op returning the builder. CallQueryOptimizer::optimizeQuery()directly. - Removed:
Utils::buildSearchConditions()(deprecated since 1.86.3). Use the query builder:// Before $conditions = Utils::buildSearchConditions($search, ['name', 'email']); // After $query->where(function ($q) use ($search) { $q->whereContains('name', $search)->orWhereContains('email', $search); }); - MySQL with SSL now connects over TLS with certificate verification on. The
ssl.*connection config was never applied; whenDB_SSL_ENABLEDis true and at least one certificate path (DB_SSL_CA_CERT,DB_SSL_CLIENT_CERT,DB_SSL_CLIENT_KEY) is set, it now maps toPDO::MYSQL_ATTR_SSL_*. SetDB_SSL_VERIFY_CERT=falseif your server's certificate can't be verified.
ORM
forceDelete()really deletes. Instance and builderforceDelete()issue a realDELETE(they used to soft-delete); builderrestore()updates only trashed rows; after a softdelete()the instance keepsexists = true(and reportstrashed()), so it can still be restored or force-deleted. Check code that calledforceDelete()expecting a soft delete.- Model event callbacks that return
falsecancel the write. Afalsefromsaving/creating/updating/deleting/restoring/forceDeletingnow stops it, andsave()/delete()/restore()/forceDelete()returnfalse. Make sure no callback returnsfalseby accident. Thetrashed,restoring,restored,forceDeletingandforceDeletedcallbacks, which were dropped, now run. - ORM
paginate()['data']is aGlueful\Database\ORM\Collectionof models, not a list of arrays, and honourswith()/withCount(). The other keys are unchanged.$result = User::query($context)->paginate(page: 1, perPage: 25); // Before $name = $result['data'][0]['name']; // After $name = $result['data'][0]->name; $rows = $result['data']->toArray(); // if you need arraysJsonResource::collection()/ModelResource::collection()accept the collection as-is. whereHas()/has()closures receive the related model's ORMBuilder(unknown methods still pass through to the QueryBuilder), and the closure is actually applied.withCount()closures receive the relation, likewith(). Self-referencing relations and unsupported relation types now throw instead of returning wrong rows. Type-hinted closures may need updating:use Glueful\Database\ORM\Builder; Post::query($context)->whereHas('comments', function (Builder $q) { $q->where('approved', true); });AsEncryptedStringwrites withEncryptionService(AES-256-GCM underAPP_KEY, AAD<table>.<column>). SetAPP_KEY, and keepENCRYPTION_KEYuntil every legacy AES-256-CBC value has been re-saved (legacy values still decrypt and are re-encrypted on their next save). Models using the cast must be constructed with anApplicationContext(new User($attributes, $context), or the context-first static helpers).HasTimestamps::fromDateTime()takesmixed. A subclass override with a narrower parameter type must widen it:public function fromDateTime(mixed $date): string { … }
Logging, errors & events
LOG_LEVELfilters.LogManagerused to write every level; the environment profile's default (for exampleinfoorwarning) now drops lower-level messages. SetLOG_LEVEL=debugto keep everything.- App
Handler::mapChannel()mappings win over the framework-origin heuristic and the built-in defaults, so mapped exceptions move to your app's channel (the most specific class wins). - The container's
CacheStore/cache.storeis anEventDispatchingCacheStorewrapper that fires cache hit/miss events.instanceofchecks against a driver class fail on it; unwrap first. Driver-only methods are forwarded.// Before if ($cache instanceof RedisCacheDriver) { … } // After $store = $cache instanceof EventDispatchingCacheStore ? $cache->getInnerStore() : $cache; if ($store instanceof RedisCacheDriver) { … } #[AsListener]registers listeners, andRequestEvent,ResponseEvent,CacheHitEventandCacheMissEventare now dispatched. Listeners you annotated (or subscribed to these events) start running — check them. See Events.- Listener classes that aren't container services run, and unbuildable ones fail at
subscribe().subscribe()now autowires unregistered listener classes (they used to get a lazy reference that failed at dispatch and was skipped silently). A listener whose dependencies can't be resolved now throws when it is subscribed, at boot, instead of quietly never running. QueryExecutedEventlisteners run outside debug mode too. The event now fires whenever a listener exists (andwithPurpose()tags reach it). Make sure such listeners are cheap enough for production traffic.
Queue, scheduler & notifications
- Apps with their own
config/schedule.phpmust add thescheduled_notificationsjob. Yourschedule.jobslist replaces the framework's, so without it notifications sent with ascheduleare still never delivered:// config/schedule.php → 'jobs' [ 'name' => 'scheduled_notifications', 'schedule' => '* * * * *', 'handler_class' => 'Glueful\\Queue\\Jobs\\ScheduledNotificationsJob', 'parameters' => ['options' => ['limit' => 50]], 'description' => 'Deliver scheduled notifications that are due', 'enabled' => env('SCHEDULED_NOTIFICATIONS_ENABLED', true), ],
It is a no-op whenNOTIFICATIONS_DATABASE_STORE=false. - Apps with their own
config/schedule.phpmust also add thequeue_maintenancejob to get automatic failed-job pruning (pastqueue.failed.retention_days):// config/schedule.php → 'jobs' [ 'name' => 'queue_maintenance', 'schedule' => '15 4 * * *', 'handler_class' => 'Glueful\\Queue\\Jobs\\QueueMaintenanceJob', 'description' => 'Prune failed jobs past queue.failed.retention_days and stale worker records', 'enabled' => env('QUEUE_MAINTENANCE_ENABLED', true), ], queue:scheduler add|remove|enable|disable|historyare gone, with their options (--job-name,--cron,--command,--description,--timeout,--history). They only printed "not yet implemented". Manage jobs inconfig/schedule.php; the remaining actions arerun,work,list,statusandhealth. Update any scripts that called the removed actions — they now fail.queue:scheduler --parallel(-p) is gone. It was accepted and ignored; remove it from scripts and supervisor configs.- Removed:
FailedJobProvider::setMaxRetries()/getMaxRetries()(deprecated since 1.86, never enforced). Delete the calls. The container'sFailedJobProvidernow honoursqueue.failed.*config. - Persistent scheduled jobs no longer duplicate. If a
persistence => truejob left duplicatescheduled_jobsrows, the next scheduler load removes them. queue.failed.retention_daysis read for failed-job pruning (it was hardcoded to 30) unlessqueue.maintenance.failed_jobs_cleanup_daysis set.- Locks created without a TTL expire after
lock.ttl(LOCK_TTL, 300s) instead of never. Pass an explicit TTL for anything that must be held longer. See Distributed Locks.
Config removals
Delete these from your own config copies and .env files — no code read them:
| Removed | Notes |
|---|---|
cache.lock_ttl, cache.enable_tags, cache.distributed, cache.tags_store | The file driver now supports tags natively. |
lock.retry.times (LOCK_RETRY_TIMES) | lock.retry.max_wait bounds the wait. |
capabilities.metrics (METRICS_DATABASE_STORE) | Core migrations install unconditionally since 1.79. |
capabilities.scheduler (SCHEDULE_DATABASE_STORE) | Same. |
observability.tracing.driver (TRACING_DRIVER) and the empty drivers block | Only the no-op tracer exists. |
observability.tracing.service_name / service_version (TRACING_SERVICE_NAME / TRACING_SERVICE_VERSION) | Nothing read them. |
Webhooks
- Run
php glueful migrate:runfor the newwebhooksmigration leaf (glueful/framework:webhooks), which ownswebhook_subscriptions/webhook_deliveries. The runtime DDL inWebhookDispatcheris gone; databases whose tables the old lazy path created migrate cleanly. WebhookDeliveryServiceuses the subscription signing scheme.# Before X-Glueful-Signature: sha256=<hmac of body> X-Glueful-Timestamp: <ts> # After X-Webhook-Signature: t=<ts>,v1=<hmac of "<ts>.<body>"> X-Webhook-Timestamp: <ts>
Update receivers to verify withWebhook::verify($payload, $signature, $secret). The header name and algorithm followapi.webhooks.signature_header/signature_algorithm.- New management routes under
/api/v1/webhooks/...(subscriptions,deliveries, and theirrotate-secret,test,statsandretryactions) requireauthplussystem.config.viewfor reads orsystem.config.editfor writes. Remove any app routes that mountedWebhookControlleron the same paths. - Events marked
#[Webhookable]or usingDispatchesWebhooksnow send webhooks.WebhookEventListeneris registered at boot (whenevents.enabledandapi.webhooks.enabledare on). If you registered it yourself, remove that registration or each event dispatches twice. WebhookDispatcher's constructor drops its unused$connectionargument, andgetSchema(),areTablesEnsured()andresetTablesEnsured()are removed:// Before new WebhookDispatcher($connection, $payloadBuilder, $context); // After new WebhookDispatcher($payloadBuilder, $context);- Deprecated:
#[Webhookable]'sonly/exceptarguments — they never had an effect.
See Webhooks.
Extensions & scaffolding
create:extensionscaffolds an enable-able package. It now writes anextra.glueful.migrationsdeclaration (a defaulton_enablelane at the flatmigrations/folder) and no longer createsdatabase/migrationsor callsloadMigrationsFrom(). An extension scaffolded earlier needs the declaration added by hand beforeextensions:enablewill run its migrations:{ "extra": { "glueful": { "migrations": [ { "id": "default", "path": "migrations", "priority": "default", "mode": "on_enable" } ] } } }
A package with no schema declares"migrations": "none"instead. Move files fromdatabase/migrations/tomigrations/and drop the provider'sloadMigrationsFrom()call. See Extensions.scaffold:controllerwrites toapp/Controllers(App\Controllers). It used to writeapi/Controllers; controllers generated there need moving if your autoloader doesn't cover that folder.system:check --productionvalidatesJWT_KEY(it checkedJWT_SECRET, which nothing reads). A missing signing key is now reported.DB_SQLITE_DATABASE=:memory:is a real in-memory database. It used to create a file named:memory:; delete that file if one exists.
1.80 – 1.90
1.90
- The file cache defaults to
<app>/storage/cache. An app whoseconfig/cache.phpsetscache.stores.file.path(the api-skeleton does) is unaffected. Otherwise make sure the web server can writestorage/cache, or setCACHE_FILE_PATH. Old entries undervendor/glueful/framework/storagecan be deleted.
1.87
- Set
MAIL_HOSTandMAIL_FROMif your app sends mail. They no longer fall back tosmtp.mailtrap.io/[email protected]; with either unset, the email channel reports itself unavailable. - PostgreSQL searches are case-insensitive, and
%/_sent by a client match literally. A customWhereClauseInterfaceimplementation must add the four text-match methods. - Uploaded images are stripped of metadata by default. Set
UPLOADS_STRIP_EXIF=falseto keep it. - Add
blob_purgeto yourconfig/schedule.phpto purge deleted uploads. - Deprecated:
Utils::buildSearchConditions()(writes the term into raw SQL unescaped) — useQueryBuilder::whereContains().
1.86
- Config lists are replaced, not merged by position (see How to upgrade). If you
relied on framework entries past the end of your own list, add them. Add
webhook_cleanupto yourjobsto prune webhook delivery records (see Webhooks). - Delete config nothing read: the
syncandnullqueue connections; the schedule'ssettingsblock,queue_mapping, and each job'squeue,timeoutandretry_attempts;app.force_https; thesecurity.headersblock; andHSTS_HEADER,FORCE_HTTPS,SCHEDULE_QUEUE_*,MAIL_BCCfrom.env. - List your permission middleware in
permissions.enforcing_middlewareif you enforce permissions in route middleware, sopermissions:diffcounts them. security:checkcan now fail. Fix what it reports (world-readable.env, short signing secret, CORS*with credentials …).- Scheduled backups that make no dump fail their job. PostgreSQL/MySQL sites need
pg_dump/mysqldumpon the scheduler host. - Webhook "Send test event" refuses local and private addresses. Test against a public endpoint or a tunnel.
- Deprecated:
FailedJobProvider::setMaxRetries()/getMaxRetries()— nothing enforces the value.
1.85
- Bootstrap environment: if your skeleton bootstrap passes
$_ENV['APP_ENV'] ?? 'development'towithEnvironment(), passenv('APP_ENV', 'development')instead (1.85.3).
1.84
- The API reference moved from
/docsto/api-docs. Update bookmarks and tooling, or setAPI_DOCS_PATH=/docsto keep the old path. Regenerate the UI page so it loads the spec from the configured path, and mirrorAPI_DOCS_PATHinto.env.example:php glueful generate:openapi --ui
See OpenAPI.
1.83
- A non-empty
CSP_HEADERis now sent asContent-Security-Policyon every response without a policy of its own. Review it first, or setCSP_REPORT_ONLY=trueand watch the browser console.
1.82
- Production runs the compiled container. Its artifact lives in
<app>/storage/cache/container/(delete the directory to force a fresh compile);/tmp/glueful_compiled_container.phpis no longer read. Recompile a precompiled container built before 1.82 once:php glueful di:container:compile
If a service misbehaves only in production,APP_DEBUG=truerestores the runtime container for comparison.
1.81
/tmp/boot_profile.logis no longer written. If tooling tailed it, setBOOT_PROFILE_LOG=/tmp/boot_profile.log(orBOOT_PROFILE_LOG=truefor a per-user temp file).
1.80
- Every installed Glueful package must declare its schema. A package without an
extra.glueful.migrationsmanifest (descriptors, or"migrations": "none") can no longer register migration paths —loadMigrationsFrom()throwsUndeclaredSchemaException. App-local providers without a package keep the legacy append. migrate:normalize-receiptsand legacy-alias receipts are removed. Upgrades from beta-era ledgers are not supported: re-provision, or rewrite the ledgersourcevalues by hand first.
1.70 – 1.79
1.79
- Run
php glueful migrate:runonce after upgrading, before anyextensions:enable. The operation ledger (extension_operations) is a new core migration; the executor refuses (SchemaNotBootstrappedException) until it exists. - Core migrations install unconditionally (
auth,extensions,locks,metrics,notifications,queue,scheduler,uploads); capability config no longer gates schema. loadMigrationsFrom()on a declared package must match its manifest exactly.extensions:enablerefuses packages without a manifest declaration (see 1.80).
1.76
- SQLite schema changes apply for real. Migrations that silently did nothing on SQLite now take
effect or throw
UnsupportedSchemaOperationException— audit SQLite migrations that modify or drop columns or foreign keys. - No more false successes:
dropPrimary(), table-comment changes, andengine()/charset()/collation()/->primary()inside analterTable()callback now fail explicitly. A modified column's$collationfails on SQLite — gate it by driver. - SQLite rebuilds can't run on tables with bare SQL-keyword column names (
key,order, …) or while you hold an unconsumed read cursor on the same connection — close cursors before altering. - Commit-phase connection loss throws
CommitOutcomeUnknownException(wasConnectionLostException). Audit handlers that retried on the old type. - Transaction retries: tune
Connection::transaction()/db($context)->transaction()withDB_RETRY_MAX_ATTEMPTS;TransactionManager::setMaxRetries()no longer affects that path. Replay callbacks must build queries inside the callback from the supplied connection. - Interface implementors:
SchemaBuilderInterfacegainedexecuteSqliteRebuild()andexecuteSqliteNativeAlteration();TableBuilderInterfacegainedrename().
1.75
- Interface implementors:
WhereClauseInterfacegainedorWhereRaw(). Model::toJson()/Collection::toJson()throw on an unencodable payload instead of returningfalse;FileUploader::calculateChecksum()throwsUploadExceptionon an unreadable file.- An empty
keyPrefixnow getsQueryCacheService's default prefix — expect one round of query-cache misses.
1.74
- Usernames up to 255 characters validate (was 30). If you depend on the old limit, enforce it at your own input boundary.
1.73
- CSRF tokens issued to authenticated callers before 1.73 stop validating. Affected clients get
403and must fetch a new token or reload. No action for unauthenticated forms.
1.71
- API-key rotation no longer extends a predecessor's expiry — it takes the earlier of the two. To lengthen a key's life, issue a new key. See API Keys.
1.70
- SQLite/PostgreSQL databases migrated before 1.70 are missing plain indexes declared inline in
createTable(). Re-run the relevantCREATE INDEXstatements, or re-migrate dev databases.
1.52 – 1.69
1.66
- Extension installer API (1.66.1):
POST /extensions/installreturns the final result ({ status: 'installed' | 'failed', … }); theGET /extensions/install/{jobId}poll endpoint andEXTENSIONS_INSTALL_AUTO_ENABLEare removed. SetEXTENSIONS_INSTALL_PHP_BINARY/COMPOSER_BINARYif the web SAPI can't find a CLI php or composer, then runphp glueful cache:clear. - The installer is off in production unless
EXTENSIONS_INSTALL_ENABLED=trueandvendor/is writable by the web user.
1.65
->middleware('require_scope:…')is enforced fail-closed. Requests lacking the scope now get403.
1.61
- Remove
documentation.paths.route_definitionsanddocumentation.paths.extension_definitions.
1.60
php glueful installis interactive. Non-interactive callers (CI,post-create-project-cmd) must pass--quiet(use the existing.env) or--skip-database.
1.59
ServiceProvider::mountStatic()is removed (withSpaManager,StaticFileDetector,SpaProvider). UseserveFrontend():// Before — served at /extensions/foo $this->mountStatic('foo', __DIR__ . '/dist'); // After — any literal path, with an index.html deep-link fallback $this->serveFrontend('/foo', __DIR__ . '/dist'); // plain static bundle that should 404 on a miss: $this->serveFrontend('/foo', __DIR__ . '/dist', ['spaFallback' => false]);
1.57
- The docblock (
comments) OpenAPI generator is removed;reflectis the only generator. RemoveAPI_DOCS_GENERATOR/documentation.generator.@route,@summary,@requestBody,@responseand@queryParamdocblocks are no longer read — document endpoints with typed DTOs,#[ApiOperation],#[QueryParam],#[ApiRequestBody]and#[ApiResponse]. See OpenAPI and Typed I/O.
1.56
- CORS fails closed. Set
CORS_ALLOWED_ORIGINS, andCORS_SUPPORTS_CREDENTIALS=trueif you need credentials (now defaults tofalse). - Remote image fetching is opt-in: configure
image.security.allowed_domains(withglueful/media). - Queue and scheduler payloads are HMAC-signed (
QUEUE_PAYLOAD_SIGNING,QUEUE_REQUIRE_SIGNED_PAYLOADS, both defaulttrue; needsAPP_KEY). SetQUEUE_REQUIRE_SIGNED_PAYLOADS=falsetemporarily to drain unsigned rows. Stored handler classes must implementJobInterface. - Memcached: flush the cache on upgrade (stored format changed).
- JWTs must carry a valid
exp; tokens without one are rejected. - Set
TRUSTED_PROXIESbehind a load balancer, or client IPs resolve to the proxy.
1.55
#[RequiresPermission]/#[RequiresRole]are enforced. Without a permission provider bound, annotated routes return403— bind one (e.g.glueful/aegis), grant the permissions, or remove the attribute.- API keys in
?api_key=are off by default. Move clients to theX-API-Keyheader or setsecurity.api_keys.allow_query_param = true. - Signed URLs need a secret (
SIGNED_URL_SECRET,uploads.signed_urls.secret, orAPP_KEY) — use a distinct value per environment. - Broken extension bindings fail loud outside production. Fix any provider whose
services()throws or binds a bare interface (give it['class' => Concrete::class]or a factory).
1.54
- Cloud storage drivers moved to packages. Core keeps only
localandmemory; a disk using another driver throwsUnsupportedStorageDriverException.composer require glueful/storage-s3 # S3, R2, MinIO, Spaces, Wasabi composer require glueful/storage-gcs # Google Cloud Storage composer require glueful/storage-azure # Azure Blob Storage - Extension container definitions override core (last layer wins). Audit extensions that reuse a
core service id, and recompile any precompiled container:
php glueful di:container:compile --force.
1.52
Four subsystems left core. Install the package for each one you use (they auto-discover via
extra.glueful; enable them in config/extensions.php if your app gates extensions there):
| Subsystem | Restore with | Also do |
|---|---|---|
Archiving, archive:manage | composer require glueful/archive | php glueful migrate:run; imports Glueful\Services\Archive\* → Glueful\Extensions\Archive\* |
Edge cache / CDN purge, cache:purge | composer require glueful/cdn | Move cache.edge overrides to the extension's cdn config |
Supervised workers, queue:autoscale, queue:supervise | composer require glueful/queue-ops | queue.workers.{process,auto_scaling,resource_limits,resource_thresholds,supervisor} and per-queue workers/max_workers/auto_scale → queue_ops.* (same env vars) |
Image processing, thumbnails, metadata, image() | composer require glueful/media | Glueful\Services\ImageProcessor → Glueful\Extensions\Media\ImageProcessor; IMAGE_* / config/image.php are extension-owned |
Without them, behaviour degrades gracefully: queue:work runs one lean worker, uploadMedia()
returns thumb_url: null, and edge purges are no-ops — but image(), cache:purge,
archive:manage and the old queue:work sub-actions (spawn, scale, status, stop, restart,
health) simply don't exist.
1.50 – 1.51
These predate the main range of this guide; see the Release Archive for detail.
- 1.51:
ChannelManager::getAvailableChannels()→getRegisteredChannelNames()(no alias). Notification retry options move from theemailnotificationconfig key tonotifications.retry. - 1.50: the user store left core. Install and enable a user store, or authentication stays
disabled (core binds a fail-closed
NullUserProvider):composer require glueful/usersGlueful\Models\UserandGlueful\Repository\UserRepositoryare gone from core — read users throughUserProviderInterface.api_keys.user_idis renameduser_uuid. See Identity.