From 865fc6cc909f3733878cb602d41a49e9ee10d70f Mon Sep 17 00:00:00 2001 From: thekevinm Date: Mon, 4 May 2026 21:04:37 +0000 Subject: [PATCH] Laravel 13 compatibility Wave 0 of the 53-package Laravel 11 -> 13 upgrade campaign. Updates df-core to compose, autoload, and runtime-load cleanly under Laravel 13.7 while preserving the package's framework-agnostic require list and its public API for downstream consumers (df-system, df-user, df-aws, and ~50 other packages). Composer: - Bump php to ^8.3 (matches Laravel 13 minimum + main app PR #578). - Drop unused doctrine/dbal (zero src/ usage). - Add laravel/helpers ^1.8 (production polyfill for the 346 sites of array_get/camel_case/array_except still in df-core). - Widen symfony/yaml to ^6.0|^7.0. - Add Laravel 13 dev-stack: laravel/framework ^13.7 (require-dev to keep df-core framework-agnostic), phpunit/phpunit ^11.5.3, mockery/mockery ^1.6, nunomaduro/collision ^8.6, orchestra/testbench ^11.0 (^11 is the L13-compatible track; ^10 pins L12). Source: - Http/Controllers/Controller: drop DispatchesJobs trait (removed in Laravel 11). Verified zero $this->dispatch() callers in df-core. - LaravelServiceProvider: replace include __DIR__/../routes/routes.php with $this->loadRoutesFrom() (idiomatic + handles route-cache internally); gate Route::prependMiddlewareToGroup('web', ...) with Route::hasMiddlewareGroup('web') (L13's API-only stack may not register the web group); gate MongoDB and CORS provider registrations with class_exists() so missing-package degrades gracefully. - Providers/CorsServiceProvider: drop unused Kernel $kernel parameter from boot() (and its `use Illuminate\Contracts\Http\Kernel;`). Confirmed Laravel 13.7's HandleCors middleware still typehints Fruitcake\Cors\CorsService, so the singleton binding remains correct; CorsService class is unchanged. - Models/NoDbModel: keep getDates()/getDateFormat() overrides but harden them. The HasAttributes trait calls $this->getDates() during attributesToArray()->addDateAttributesToArray(), and the trait's own implementation depends on usesTimestamps() which NoDbModel cannot satisfy (it doesn't extend Model and doesn't use HasTimestamps). Without the override the call chain reaches usesTimestamps() and fatal-errors. Stub implementations now defend against $dates / $dateFormat being unset. - Database/Connectors/SQLiteConnector: add `: \PDO` covariant return type to connect() (parent has no return type; widening allowed). - Testing/TestCase: rename setupBeforeClass -> setUpBeforeClass (PHPUnit 11 is case-sensitive). Replace the broken cwd-relative require './bootstrap/app.php' with a path resolver that walks up from this file looking for bootstrap/app.php + artisan, and supports DF_TEST_BOOTSTRAP env override. - helpers.php: replace internal array_get() calls inside df-core's own helper functions with Illuminate\Support\Arr::get() so the package's helpers do not require the laravel/helpers polyfill at runtime. The 346 helper sites elsewhere in df-core stay polyfilled (out of scope for this PR). Verified: - composer validate --strict: passes. - composer install (with path-repo aliases for the structural df-system <-> df-core circular require): resolves Laravel 13.7.0, PHPUnit 11.5.55, testbench v11.1.0. - php -l on every src/ + tests/ file: zero parse errors. - Smoke test: 10/10 critical df-core classes (Controller, BaseModel, NoDbModel, BaseSystemLookup, Builder, DfCorsService, SQLiteConnector, CorsServiceProvider, LaravelServiceProvider, TestCase) load via reflection; NoDbModel and BaseModel toArray() runs without fatal. --- composer.json | 12 +++-- src/Database/Connectors/SQLiteConnector.php | 2 +- src/Http/Controllers/Controller.php | 3 +- src/LaravelServiceProvider.php | 26 +++++---- src/Models/NoDbModel.php | 16 ++++-- src/Providers/CorsServiceProvider.php | 4 +- src/Testing/TestCase.php | 58 +++++++++++++++++++-- src/helpers.php | 10 ++-- 8 files changed, 100 insertions(+), 31 deletions(-) diff --git a/composer.json b/composer.json index 26d5c933..5ef5fbb5 100644 --- a/composer.json +++ b/composer.json @@ -30,15 +30,19 @@ "minimum-stability": "dev", "prefer-stable": true, "require": { - "php": "^8.0", - "doctrine/dbal": "^3.1.4", + "php": "^8.3", "guzzlehttp/guzzle": "^7.8", - "symfony/yaml": "^6.0", + "laravel/helpers": "^1.8", + "symfony/yaml": "^6.0|^7.0", "tymon/jwt-auth": "^2.1.0", "dreamfactory/df-system": "~0.6.2" }, "require-dev": { - "phpunit/phpunit": "@stable" + "laravel/framework": "^13.7", + "mockery/mockery": "^1.6", + "nunomaduro/collision": "^8.6", + "orchestra/testbench": "^11.0", + "phpunit/phpunit": "^11.5.3" }, "autoload": { "files": [ diff --git a/src/Database/Connectors/SQLiteConnector.php b/src/Database/Connectors/SQLiteConnector.php index 0fa93e69..6737c1de 100755 --- a/src/Database/Connectors/SQLiteConnector.php +++ b/src/Database/Connectors/SQLiteConnector.php @@ -10,7 +10,7 @@ class SQLiteConnector extends \Illuminate\Database\Connectors\SQLiteConnector * @param array $config * @return \PDO */ - public function connect(array $config) + public function connect(array $config): \PDO { $options = $this->getOptions($config); diff --git a/src/Http/Controllers/Controller.php b/src/Http/Controllers/Controller.php index d19f445e..0c48d577 100644 --- a/src/Http/Controllers/Controller.php +++ b/src/Http/Controllers/Controller.php @@ -1,11 +1,10 @@ registerOtherProviders(); - // add routes - /** @noinspection PhpUndefinedMethodInspection */ - if (!$this->app->routesAreCached()) { - include __DIR__ . '/../routes/routes.php'; - } + // add routes (loadRoutesFrom handles the route-cache check internally) + $this->loadRoutesFrom(__DIR__ . '/../routes/routes.php'); // add commands, https://laravel.com/docs/5.4/packages#commands $this->addCommands(); @@ -83,8 +80,10 @@ public function boot() */ public function register() { - // Register MongoDB provider first - $this->app->register(MongoDBServiceProvider::class); + // Register MongoDB provider first (gated — package is optional) + if (class_exists(MongoDBServiceProvider::class)) { + $this->app->register(MongoDBServiceProvider::class); + } // merge in df config, https://laravel.com/docs/5.4/packages#resources $this->mergeConfigFrom(__DIR__ . '/../config/df.php', 'df'); @@ -117,8 +116,10 @@ protected function addMiddleware() Route::aliasMiddleware('df.access_check', AccessCheck::class); Route::aliasMiddleware('df.verb_override', VerbOverrides::class); - /** Add the first user check to the web group */ - Route::prependMiddlewareToGroup('web', FirstUserCheck::class); + /** Add the first user check to the web group (gated — `web` may not be defined in API-only apps) */ + if (Route::hasMiddlewareGroup('web')) { + Route::prependMiddlewareToGroup('web', FirstUserCheck::class); + } $middleware = [ 'df.verb_override', @@ -183,7 +184,10 @@ protected function registerExtensions() protected function registerOtherProviders() { - // use CORS - $this->app->register(CorsServiceProvider::class); + // use CORS (gated — Laravel HandleCors middleware ships in framework so the + // provider class is always available, but keep the gate symmetric/defensive) + if (class_exists(CorsServiceProvider::class)) { + $this->app->register(CorsServiceProvider::class); + } } } diff --git a/src/Models/NoDbModel.php b/src/Models/NoDbModel.php index 3c652040..25e03acb 100644 --- a/src/Models/NoDbModel.php +++ b/src/Models/NoDbModel.php @@ -74,15 +74,25 @@ public function getCasts() return $casts; } - // not sure why Laravel has these are in the HasAttributes trait instead of the model, but they need simplification + // The HasAttributes trait calls $this->getDates() and $this->getDateFormat() internally + // during attributesToArray(). The trait's own getDates() implementation depends on + // usesTimestamps() / $dates, neither of which NoDbModel inherits (it doesn't extend + // Model and doesn't use HasTimestamps). We provide stub overrides that short-circuit + // the date handling so attributesToArray() works for this Model-less use case. + // + // Historical note: Eloquent's Model::getDates() was removed in Laravel 10, but the + // method still lives in HasAttributes and is invoked from inside the trait, so the + // override remains necessary as long as NoDbModel uses HasAttributes directly. public function getDates() { - return $this->dates; + return property_exists($this, 'dates') ? (array) $this->dates : []; } protected function getDateFormat() { - return $this->dateFormat; + return property_exists($this, 'dateFormat') && $this->dateFormat + ? $this->dateFormat + : 'Y-m-d H:i:s'; } /** diff --git a/src/Providers/CorsServiceProvider.php b/src/Providers/CorsServiceProvider.php index 8725df12..3ca795b0 100644 --- a/src/Providers/CorsServiceProvider.php +++ b/src/Providers/CorsServiceProvider.php @@ -7,7 +7,6 @@ use Fruitcake\Cors\CorsService; use Illuminate\Http\Middleware\HandleCors; use Illuminate\Database\QueryException; -use Illuminate\Contracts\Http\Kernel; use Illuminate\Http\Request; use Illuminate\Support\ServiceProvider; use Illuminate\Support\Str; @@ -31,10 +30,9 @@ public function register() * Add the Cors middleware to the router. * * @param Request $request - * @param Kernel $kernel * @throws \Exception */ - public function boot(Request $request, Kernel $kernel) + public function boot(Request $request) { $api_prefix = config('df.api_route_prefix', 'api'); config(['cors.paths' => [$api_prefix . '/*']]); diff --git a/src/Testing/TestCase.php b/src/Testing/TestCase.php index 625f4377..da29a0a6 100755 --- a/src/Testing/TestCase.php +++ b/src/Testing/TestCase.php @@ -37,7 +37,7 @@ class TestCase extends LaravelTestCase /** * Runs before every test class. */ - public static function setupBeforeClass(): void + public static function setUpBeforeClass(): void { echo "\n------------------------------------------------------------------------------\n"; echo "Running test: " . get_called_class() . "\n"; @@ -110,17 +110,69 @@ public function stage() /** * Creates the application. * + * Consuming applications should override this if the Laravel-style + * bootstrap path differs from the conventional `bootstrap/app.php` + * relative to the host project root. The base path is resolved from + * the consumer's working tree (parent of the running phpunit binary) + * rather than the current working directory, so tests can be invoked + * from any cwd. + * * @return \Illuminate\Foundation\Application */ public function createApplication() { - $app = require './bootstrap/app.php'; + $bootstrap = $this->resolveBootstrapPath(); + + /** @var \Illuminate\Foundation\Application $app */ + $app = require $bootstrap; - $app->make('Illuminate\Contracts\Console\Kernel')->bootstrap(); + $app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap(); return $app; } + /** + * Resolve the path to the Laravel application bootstrap file. + * + * Order of resolution: + * 1. DF_TEST_BOOTSTRAP env var (explicit override) + * 2. df-core's own ./bootstrap/app.php (when df-core has its own) + * 3. Walk up from this file looking for a vendor/.. host project root + * + * @return string + */ + protected function resolveBootstrapPath(): string + { + if ($override = getenv('DF_TEST_BOOTSTRAP')) { + return $override; + } + + // df-core itself: __DIR__ = src/Testing, so root is two levels up. + $localCandidate = dirname(__DIR__, 2) . '/bootstrap/app.php'; + if (file_exists($localCandidate)) { + return $localCandidate; + } + + // Installed under a host project's vendor/dreamfactory/df-core/src/Testing + // → walk up to the host project root (parent of vendor/). + $dir = __DIR__; + for ($i = 0; $i < 10; $i++) { + $candidate = $dir . '/bootstrap/app.php'; + if (file_exists($candidate) && is_file($dir . '/artisan')) { + return $candidate; + } + $parent = dirname($dir); + if ($parent === $dir) { + break; + } + $dir = $parent; + } + + throw new \RuntimeException( + 'Unable to locate bootstrap/app.php. Set DF_TEST_BOOTSTRAP env var to its absolute path.' + ); + } + /** * @param $verb * @param $url diff --git a/src/helpers.php b/src/helpers.php index 9fecf459..b5e7c502 100644 --- a/src/helpers.php +++ b/src/helpers.php @@ -1,5 +1,7 @@