Upgrading

Step-by-step upgrade guide for the Glueful framework — the actions each release requires, from 1.91 back to 1.50.

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

  1. Update the package.
    composer update glueful/framework
    
  2. 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.
  3. 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 need php glueful commands:cache on 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:clear never touches the command manifest.
  4. Compare your config/ copies with the framework's. Since 1.86, a list your app's config sets (a config/schedule.php jobs list, 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 csrf middleware was built from constructor defaults, so config/security.php's csrf block was ignored. With the shipped config:
    • tokens expire after 3600s (was 7200s) — CSRF_TOKEN_LIFETIME;
    • the exemptRoutes list (auth/login, auth/register, webhooks/inbound/*, public/*, csrf-token, …) now skips CSRF;
    • CSRF_PROTECTION_ENABLED and CSRF_DOUBLE_SUBMIT take effect.

    Review your app's security.csrf block before upgrading. If your app has its own config/security.php, its exemptRoutes list replaces the framework's.
  • exemptRoutes entries match under the API prefix. Short entries such as auth/login now 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 was webhooks/*, 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 own exemptRoutes entry. If your app has its own config/security.php, change webhooks/* there too.
  • Cookie-transport writes with no Origin / Referer get 403 CSRF_ORIGIN_MISMATCH.skip_for_bearer_auth used to skip the origin check because the Authorization header had been injected by session_cookie. Browsers send Origin on 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/refresh and /auth/session/logout to accept Sec-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 anchored CORS_ALLOWED_ORIGIN_PATTERNS match) with credentials on does.
  • CORS origin patterns apply to real responses, not just preflight. Origins matching CORS_ALLOWED_ORIGIN_PATTERNS (and every origin when development_allow_all is on in development) now get Access-Control-Allow-Origin on 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's scopes claim, a scope array or space-delimited scope string ("read:posts write:posts"), the user record's scopes, or an identity.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/session from existing .env files 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 your config/permissions.php before 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. The rate_limit middleware is now attached automatically (never duplicated if you added rate_limit, rate_limit:… or enhanced_rate_limit yourself), so these routes can return 429 with 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|endpoint keys by user or endpoint instead of always by IP. Unknown values fall back to ip.
  • #[Fields] routes project their responses by ?fields= / ?expand= (the field_selection middleware is attached automatically) and return 400 for strict whitelist violations. useAdvancedPatterns: true now makes allowed act as patterns (posts.*, -password). See Field Selection.
  • csrf: and request_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 tracing middleware follows TRACING_ENABLED (default false) and passes requests straight through when disabled. Set TRACING_ENABLED=true if you bind a real tracer.
  • API_RATE_LIMITING_ENABLED=false and api.field_selection.enabled = false switch those features off. Neither key was read, so rate limits and field selection always applied. Check apps that set them to false.
  • OpenAPI tags skip the API prefix. Under /api/v1 every 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 with php glueful generate:openapi. See OpenAPI.

Validation & database

  • unique: and exists: rules are enforced. They silently passed before, so requests that used to pass may now get 422. The rules fail closed: with no connection or context they throw LogicException. A Validator you 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 call setConnection() on the rule.) Database-backed custom rules can implement the new Glueful\Validation\Contracts\DatabaseAwareRule to receive the Validator's connection. See Validation.
  • nullable and sometimes take effect. A null or empty value stops the rules after nullable; an absent sometimes field skips all its rules and no longer appears in filtered() / validated().
  • useCurrentOnUpdate() throws UnsupportedSchemaOperationException on PostgreSQL and SQLite instead of being silently ignored (MySQL now emits ON 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 typed Connection|PDO|mysqli. Pass a Glueful Connection (now supported) or a raw driver handle.
  • SQLite unique violations return 409, not 500. Adjust tests that expected a 500.
  • Deprecated: QueryBuilder::optimize() — it never had an effect and is now a no-op returning the builder. Call QueryOptimizer::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; when DB_SSL_ENABLED is true and at least one certificate path (DB_SSL_CA_CERT, DB_SSL_CLIENT_CERT, DB_SSL_CLIENT_KEY) is set, it now maps to PDO::MYSQL_ATTR_SSL_*. Set DB_SSL_VERIFY_CERT=false if your server's certificate can't be verified.

ORM

  • forceDelete() really deletes. Instance and builder forceDelete() issue a real DELETE (they used to soft-delete); builder restore() updates only trashed rows; after a soft delete() the instance keeps exists = true (and reports trashed()), so it can still be restored or force-deleted. Check code that called forceDelete() expecting a soft delete.
  • Model event callbacks that return false cancel the write. A false from saving / creating / updating / deleting / restoring / forceDeleting now stops it, and save() / delete() / restore() / forceDelete() return false. Make sure no callback returns false by accident. The trashed, restoring, restored, forceDeleting and forceDeleted callbacks, which were dropped, now run.
  • ORM paginate()['data'] is a Glueful\Database\ORM\Collection of models, not a list of arrays, and honours with() / 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 arrays
    

    JsonResource::collection() / ModelResource::collection() accept the collection as-is.
  • whereHas() / has() closures receive the related model's ORM Builder (unknown methods still pass through to the QueryBuilder), and the closure is actually applied. withCount() closures receive the relation, like with(). 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);
    });
    
  • AsEncryptedString writes with EncryptionService (AES-256-GCM under APP_KEY, AAD <table>.<column>). Set APP_KEY, and keep ENCRYPTION_KEY until 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 an ApplicationContext (new User($attributes, $context), or the context-first static helpers).
  • HasTimestamps::fromDateTime() takes mixed. A subclass override with a narrower parameter type must widen it:
    public function fromDateTime(mixed $date): string { … }
    

Logging, errors & events

  • LOG_LEVEL filters. LogManager used to write every level; the environment profile's default (for example info or warning) now drops lower-level messages. Set LOG_LEVEL=debug to 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.store is an EventDispatchingCacheStore wrapper that fires cache hit/miss events. instanceof checks 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, and RequestEvent, ResponseEvent, CacheHitEvent and CacheMissEvent are 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.
  • QueryExecutedEvent listeners run outside debug mode too. The event now fires whenever a listener exists (and withPurpose() tags reach it). Make sure such listeners are cheap enough for production traffic.

Queue, scheduler & notifications

  • Apps with their own config/schedule.php must add the scheduled_notifications job. Your schedule.jobs list replaces the framework's, so without it notifications sent with a schedule are 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 when NOTIFICATIONS_DATABASE_STORE=false.
  • Apps with their own config/schedule.php must also add the queue_maintenance job to get automatic failed-job pruning (past queue.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|history are gone, with their options (--job-name, --cron, --command, --description, --timeout, --history). They only printed "not yet implemented". Manage jobs in config/schedule.php; the remaining actions are run, work, list, status and health. 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's FailedJobProvider now honours queue.failed.* config.
  • Persistent scheduled jobs no longer duplicate. If a persistence => true job left duplicate scheduled_jobs rows, the next scheduler load removes them.
  • queue.failed.retention_days is read for failed-job pruning (it was hardcoded to 30) unless queue.maintenance.failed_jobs_cleanup_days is 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:

RemovedNotes
cache.lock_ttl, cache.enable_tags, cache.distributed, cache.tags_storeThe 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 blockOnly 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:run for the new webhooks migration leaf (glueful/framework:webhooks), which owns webhook_subscriptions / webhook_deliveries. The runtime DDL in WebhookDispatcher is gone; databases whose tables the old lazy path created migrate cleanly.
  • WebhookDeliveryService uses 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 with Webhook::verify($payload, $signature, $secret). The header name and algorithm follow api.webhooks.signature_header / signature_algorithm.
  • New management routes under /api/v1/webhooks/... (subscriptions, deliveries, and their rotate-secret, test, stats and retry actions) require auth plus system.config.view for reads or system.config.edit for writes. Remove any app routes that mounted WebhookController on the same paths.
  • Events marked #[Webhookable] or using DispatchesWebhooks now send webhooks.WebhookEventListener is registered at boot (when events.enabled and api.webhooks.enabled are on). If you registered it yourself, remove that registration or each event dispatches twice.
  • WebhookDispatcher's constructor drops its unused $connection argument, and getSchema(), areTablesEnsured() and resetTablesEnsured() are removed:
    // Before
    new WebhookDispatcher($connection, $payloadBuilder, $context);
    
    // After
    new WebhookDispatcher($payloadBuilder, $context);
    
  • Deprecated: #[Webhookable]'s only / except arguments — they never had an effect.

See Webhooks.

Extensions & scaffolding

  • create:extension scaffolds an enable-able package. It now writes an extra.glueful.migrations declaration (a default on_enable lane at the flat migrations/ folder) and no longer creates database/migrations or calls loadMigrationsFrom(). An extension scaffolded earlier needs the declaration added by hand before extensions:enable will 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 from database/migrations/ to migrations/ and drop the provider's loadMigrationsFrom() call. See Extensions.
  • scaffold:controller writes to app/Controllers (App\Controllers). It used to write api/Controllers; controllers generated there need moving if your autoloader doesn't cover that folder.
  • system:check --production validates JWT_KEY (it checked JWT_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 whose config/cache.php sets cache.stores.file.path (the api-skeleton does) is unaffected. Otherwise make sure the web server can write storage/cache, or set CACHE_FILE_PATH. Old entries under vendor/glueful/framework/storage can be deleted.

1.87

  • Set MAIL_HOST and MAIL_FROM if your app sends mail. They no longer fall back to smtp.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 custom WhereClauseInterface implementation must add the four text-match methods.
  • Uploaded images are stripped of metadata by default. Set UPLOADS_STRIP_EXIF=false to keep it.
  • Add blob_purge to your config/schedule.php to purge deleted uploads.
  • Deprecated: Utils::buildSearchConditions() (writes the term into raw SQL unescaped) — use QueryBuilder::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_cleanup to your jobs to prune webhook delivery records (see Webhooks).
  • Delete config nothing read: the sync and null queue connections; the schedule's settings block, queue_mapping, and each job's queue, timeout and retry_attempts; app.force_https; the security.headers block; and HSTS_HEADER, FORCE_HTTPS, SCHEDULE_QUEUE_*, MAIL_BCC from .env.
  • List your permission middleware in permissions.enforcing_middleware if you enforce permissions in route middleware, so permissions:diff counts them.
  • security:check can 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 / mysqldump on 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' to withEnvironment(), pass env('APP_ENV', 'development') instead (1.85.3).

1.84

  • The API reference moved from /docs to /api-docs. Update bookmarks and tooling, or set API_DOCS_PATH=/docs to keep the old path. Regenerate the UI page so it loads the spec from the configured path, and mirror API_DOCS_PATH into .env.example:
    php glueful generate:openapi --ui
    

    See OpenAPI.

1.83

  • A non-empty CSP_HEADER is now sent as Content-Security-Policy on every response without a policy of its own. Review it first, or set CSP_REPORT_ONLY=true and 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.php is 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=true restores the runtime container for comparison.

1.81

  • /tmp/boot_profile.log is no longer written. If tooling tailed it, set BOOT_PROFILE_LOG=/tmp/boot_profile.log (or BOOT_PROFILE_LOG=true for a per-user temp file).

1.80

  • Every installed Glueful package must declare its schema. A package without an extra.glueful.migrations manifest (descriptors, or "migrations": "none") can no longer register migration paths — loadMigrationsFrom() throws UndeclaredSchemaException. App-local providers without a package keep the legacy append.
  • migrate:normalize-receipts and legacy-alias receipts are removed. Upgrades from beta-era ledgers are not supported: re-provision, or rewrite the ledger source values by hand first.

1.70 – 1.79

1.79

  • Run php glueful migrate:run once after upgrading, before any extensions: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:enable refuses 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, and engine() / charset() / collation() / ->primary() inside an alterTable() callback now fail explicitly. A modified column's $collation fails 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 (was ConnectionLostException). Audit handlers that retried on the old type.
  • Transaction retries: tune Connection::transaction() / db($context)->transaction() with DB_RETRY_MAX_ATTEMPTS; TransactionManager::setMaxRetries() no longer affects that path. Replay callbacks must build queries inside the callback from the supplied connection.
  • Interface implementors: SchemaBuilderInterface gained executeSqliteRebuild() and executeSqliteNativeAlteration(); TableBuilderInterface gained rename().

1.75

  • Interface implementors: WhereClauseInterface gained orWhereRaw().
  • Model::toJson() / Collection::toJson() throw on an unencodable payload instead of returning false; FileUploader::calculateChecksum() throws UploadException on an unreadable file.
  • An empty keyPrefix now gets QueryCacheService'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 403 and 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 relevant CREATE INDEX statements, or re-migrate dev databases.

1.52 – 1.69

1.66

  • Extension installer API (1.66.1): POST /extensions/install returns the final result ({ status: 'installed' | 'failed', … }); the GET /extensions/install/{jobId} poll endpoint and EXTENSIONS_INSTALL_AUTO_ENABLE are removed. Set EXTENSIONS_INSTALL_PHP_BINARY / COMPOSER_BINARY if the web SAPI can't find a CLI php or composer, then run php glueful cache:clear.
  • The installer is off in production unless EXTENSIONS_INSTALL_ENABLED=true and vendor/ is writable by the web user.

1.65

  • ->middleware('require_scope:…') is enforced fail-closed. Requests lacking the scope now get 403.

1.61

  • Remove documentation.paths.route_definitions and documentation.paths.extension_definitions.

1.60

  • php glueful install is 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 (with SpaManager, StaticFileDetector, SpaProvider). Use serveFrontend():
    // 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; reflect is the only generator. Remove API_DOCS_GENERATOR / documentation.generator. @route, @summary, @requestBody, @response and @queryParam docblocks 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, and CORS_SUPPORTS_CREDENTIALS=true if you need credentials (now defaults to false).
  • Remote image fetching is opt-in: configure image.security.allowed_domains (with glueful/media).
  • Queue and scheduler payloads are HMAC-signed (QUEUE_PAYLOAD_SIGNING, QUEUE_REQUIRE_SIGNED_PAYLOADS, both default true; needs APP_KEY). Set QUEUE_REQUIRE_SIGNED_PAYLOADS=false temporarily to drain unsigned rows. Stored handler classes must implement JobInterface.
  • Memcached: flush the cache on upgrade (stored format changed).
  • JWTs must carry a valid exp; tokens without one are rejected.
  • Set TRUSTED_PROXIES behind a load balancer, or client IPs resolve to the proxy.

1.55

  • #[RequiresPermission] / #[RequiresRole] are enforced. Without a permission provider bound, annotated routes return 403 — 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 the X-API-Key header or set security.api_keys.allow_query_param = true.
  • Signed URLs need a secret (SIGNED_URL_SECRET, uploads.signed_urls.secret, or APP_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 local and memory; a disk using another driver throws UnsupportedStorageDriverException.
    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):

SubsystemRestore withAlso do
Archiving, archive:managecomposer require glueful/archivephp glueful migrate:run; imports Glueful\Services\Archive\* → Glueful\Extensions\Archive\*
Edge cache / CDN purge, cache:purgecomposer require glueful/cdnMove cache.edge overrides to the extension's cdn config
Supervised workers, queue:autoscale, queue:supervisecomposer require glueful/queue-opsqueue.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/mediaGlueful\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 the emailnotification config key to notifications.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/users
    

    Glueful\Models\User and Glueful\Repository\UserRepository are gone from core — read users through UserProviderInterface. api_keys.user_id is renamed user_uuid. See Identity.