Skip to content

Conversation

@LukeTowers
Copy link
Member

@LukeTowers LukeTowers commented Feb 25, 2025

Replaces #173, continuing the work done by @mjauvin @bennothommo & @wverhoogt

Summary by CodeRabbit

  • New Features

    • Native MariaDB support and broader cross-database schema change handling.
    • Configurable auth password attribute and fluent authentication method chaining.
    • publicPath() now accepts an optional subpath.
  • Bug Fixes

    • Simplified maintenance handling to consistent 503 responses.
    • Improved robustness for SSL host verification and section parsing.
  • Refactor

    • Database layer modernized and pagination accepts an optional total.
  • Chores

    • CI/workflow, PHP requirement, dependency, and test tooling updates.

@LukeTowers LukeTowers marked this pull request as ready for review July 27, 2025 14:38
LukeTowers and others added 4 commits July 28, 2025 00:42
Laravel 10 made it so that raw expression objects cannot be cast directly into strings anymore. There will be other places that will need to be updated.
@coderabbitai
Copy link

coderabbitai bot commented Sep 30, 2025

Walkthrough

Updates CI workflows and tooling, raises PHP/dependency constraints, expands PHPStan baseline, and introduces extensive database refactors: new PDO/DBAL drivers, connection trait, per-driver connection classes and schema grammars. Also adds model/trait adjustments, Foundation/API tweaks, many test-suite refactors and new grammar tests.

Changes

Cohort / File(s) Summary
CI workflows
.github/workflows/code-analysis.yaml, .github/workflows/code-quality.yaml, .github/workflows/tests.yml
Add Composer GitHub token step; clear phpstan cache; narrow triggers; reduce test concurrency; run tests via composer.
Tooling & config
composer.json, phpunit.xml.dist, phpstan.neon, phpstan-baseline.neon, .gitignore
Bump PHP to ^8.2; upgrade many deps (Laravel 12, DBAL3, PHPUnit11, etc.); adjust scripts; extend phpstan baseline and config; add gitignore entries.
Database core & connections
src/Database/Connections/Connection.php, src/Database/Connections/HasConnection.php, src/Database/Connections/*Connection.php (MySql/Postgres/SQLite/SqlServer/MariaDb), src/Database/Connectors/ConnectionFactory.php, src/Database/Connectors/SQLiteConnector.php
Make Connection abstract; extract HasConnection trait with query/logging/Doctrine plumbing; add MariaDbConnection; adapt driver base classes to use HasConnection; remove custom SQLiteConnector file; factory handles mariadb.
Doctrine PDO layer
src/Database/PDO/Connection.php, src/Database/PDO/ConnectsToDatabase.php, src/Database/PDO/*Driver.php, src/Database/PDO/SqlServerConnection.php
Add PDO wrapper, connector trait, and PDO drivers (MySql/Postgres/SQLite/SqlServer) plus SqlServerConnection adapter implementing DBAL interfaces.
Schema grammars & helpers
src/Database/Schema/Grammars/*, src/Database/Schema/Grammars/Concerns/MySqlBasedGrammar.php, src/Database/Query/Grammars/*
New Winter\Storm schema grammars for MySQL/MariaDB/Postgres/SQLite/SqlServer with compileChange and getDefaultValue; shared MySqlBasedGrammar trait; query grammar additions/fixes (MariaDbGrammar + alias fixes).
Query builder / builder changes
src/Database/Builder.php, src/Database/QueryBuilder.php
Handle Expression in searchWhereInternal; paginate gains $total parameter; upsert delegates to parent; adjusted runPaginationCountQuery.
Model & attributes
src/Database/Model.php, src/Database/Concerns/HasAttributes.php, src/Database/Relations/Concerns/DeferOneOrMany.php, src/Database/Traits/Revisionable.php
Add HasAttributes trait and getDates(); remove Model::hasAttribute; refine deferred key return type; broaden date-detection logic in revisionable.
Database service/provider
src/Database/DatabaseServiceProvider.php, src/Database/MigrationServiceProvider.php
Register db.transactions singleton and refactor connection service registration; blueprint resolver signature now accepts Connection. Migration provider now extends the aliased base.
Config & parsing
src/Config/ConfigWriter.php, src/Config/Repository.php
Switch to PhpParser Php8 parser with host version; refine $items docblock.
Auth
src/Auth/Manager.php, src/Auth/Models/User.php, src/Auth/Models/Throttle.php, src/Auth/Models/{Group,Role,Preferences}.php
setUser() becomes fluent (returns static); User gains dynamic authPasswordName/getAuthPasswordName() and login hooks; Throttle uses $casts for timestamps; docblock/type tweaks across models.
Foundation & HTTP
src/Foundation/Application.php, src/Foundation/Bootstrap/LoadConfiguration.php, src/Foundation/Http/Kernel.php, src/Foundation/Http/Middleware/CheckForMaintenanceMode.php
publicPath($path='') added; register ContextServiceProvider; rename $routeMiddleware→$middlewareAliases; simplify maintenance handling; add phpstan ignores in bootstrap.
Console & CLI
src/Console/Traits/HandlesCleanup.php
handleSignal signature now typed with int
Halcyon & caching
src/Halcyon/*, src/Halcyon/MemoryCacheManager.php
Minor docs/return refinements; MemoryCacheManager::repository accepts optional $config; explicit null returns on delete when not exists.
Schema grammars tests & test harness
tests/GrammarTestCase.php, tests/Database/Schema/Grammars/*SchemaGrammarTest.php
Add GrammarTestCase helper and new schema grammar unit tests for MySQL, Postgres, SQLite, SQL Server.
Tests — global refactor
tests/**/* (many files), tests/bootstrap.php, tests/stubs/Facades.stub
Unify tests to extend fully-qualified \Winter\Storm\Tests{TestCase,DbTestCase}; remove global class_aliases; adopt onlyMethods for mocks; add attributes/data-provider updates; update many test method/property signatures; remove facade stubs.
Mailer, facades & helpers
src/Mail/Mailer.php, src/Support/Facades/Mail.php, src/Support/Facades/Flash.php, src/Support/helpers-array.php, src/Support/Facades/DB.php, src/Support/Facades/File.php
Explicit null returns in Mailer::send; Mail::fake requires MailManager; Flash::forget documented fluent return; remove array_first/last, change array_shuffle signature; DB facade type updates; add FilesystemAdapter import.
Misc small changes
src/Console/*, src/Database/PDO/*, src/Network/Http.php, src/Support/Traits/Emitter.php, many small docblocks and formatting edits
Curl SSL_VERIFYHOST uses integer 0; PHPDoc/type refinements; minor refactors and formatting consistency updates across many files.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant App as Application
  participant Conn as Winter Connection
  participant Trait as HasConnection
  participant Grammar as SchemaGrammar
  participant DBAL as Doctrine Driver
  participant PDO as PDO

  App->>Conn: request query()/getDoctrineConnection()
  Conn->>Trait: delegate query/logging/getDoctrineConnection
  Trait->>Grammar: use grammar & post-processor for builder
  alt Doctrine available
    Trait->>DBAL: register/connect using PDO
    DBAL->>PDO: use wrapped PDO instance
    DBAL-->>Conn: return DBAL connection/adapters
  else
    Trait-->>Conn: using native schema operations
  end
Loading
sequenceDiagram
  autonumber
  participant Dev as Migration/Blueprint
  participant Conn as Connection
  participant Gram as SchemaGrammar
  participant DB as Database

  Dev->>Conn: run Blueprint->change()
  Conn->>Gram: compileChange(blueprint, command)
  Gram->>Conn: query schema (columns, indexes, fks)
  Gram->>Gram: build ALTER statements per-column
  Gram-->>Conn: SQL statements
  Conn->>DB: execute statements
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120+ minutes

Areas to focus on:

  • Database connection/trait/driver interactions (HasConnection, PDO wrappers, per-driver classes).
  • Schema grammar compileChange implementations and their compatibility with schema builders.
  • Composer dependency and PHP requirement bumps (impact on codebase compatibility).
  • Large test-suite mass edits (bootstrap class_alias removal, many test changes) and new grammar tests.
  • phpstan baseline additions (suppressions may hide issues).

Poem

I nibbled wires, flipped some deps, and hopped through PDO,
New grammars bloom like clover rows, where ALTER statements grow.
I cached my carrots (phpstan), trimmed twiggy tests with care,
Maria sings, Postgres hums — drivers weaving in the air.
Compose, commit, then bound away — CI winks, "All green!" 🐇✨

Pre-merge checks and finishing touches

✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "Support Laravel 12" directly and accurately reflects the primary change in the changeset. The entire PR centers on upgrading the Storm library to support Laravel 12, which encompasses extensive changes across the codebase including PHP version requirements, dependency version upgrades, database architecture refactors, connection and schema grammar implementations, and comprehensive test framework updates. The title is concise, clear, and specific enough that a developer scanning the repository history would immediately understand the core objective without ambiguity or vagueness.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch wip/1.3

Comment @coderabbitai help to get the list of available commands and usage tips.

@LukeTowers
Copy link
Member Author

@coderabbitai review this

@coderabbitai
Copy link

coderabbitai bot commented Oct 15, 2025

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Get PR information
echo "=== PR Information ==="
gh pr view 207 --json title,body,state,files,additions,deletions

echo -e "\n=== Files changed in PR ==="
gh pr diff 207 --name-only

echo -e "\n=== Commit details for a97f6552c86d916a78048753125f0d60d20b5e86 ==="
git show --stat a97f6552c86d916a78048753125f0d60d20b5e86

echo -e "\n=== Get full diff of the PR (first 500 lines) ==="
gh pr diff 207 | head -500

Length of output: 38648


🏁 Script executed:

#!/bin/bash

echo "=== Examining migration system changes ==="
gh pr diff 207 -- src/Database/MigrationServiceProvider.php

echo -e "\n=== New PDO Connection implementation ==="
cat src/Database/PDO/Connection.php | head -100

echo -e "\n=== HasConnection trait (new file) ==="
cat src/Database/Connections/HasConnection.php | head -100

echo -e "\n=== Changes to main Connection class ==="
gh pr diff 207 -- src/Database/Connections/Connection.php | head -150

echo -e "\n=== MariaDbConnection (new file) ==="
cat src/Database/Connections/MariaDbConnection.php

echo -e "\n=== Schema grammar changes ==="
gh pr diff 207 -- src/Database/Schema/Grammars/MySqlGrammar.php

Length of output: 7323


🏁 Script executed:

#!/bin/bash

echo "=== Full diff for MigrationServiceProvider ==="
gh pr diff 207 | grep -A 50 -B 5 "MigrationServiceProvider.php"

echo -e "\n=== Full diff for Connection.php ==="
gh pr diff 207 | grep -A 100 -B 5 "src/Database/Connections/Connection.php"

echo -e "\n=== Checking Laravel 12 migration changes ==="
# Let's search for what changed in migrations
fd -t f "Migration" src/Database/

echo -e "\n=== Test file changes ==="
gh pr diff 207 | grep -A 20 "tests/Database/Schema/Grammars"

echo -e "\n=== Composer dependency changes ==="
cat composer.json | grep -A 5 -B 5 "laravel/framework"

Length of output: 9009


🏁 Script executed:

#!/bin/bash

echo "=== Check Blueprint resolver changes ==="
cat src/Database/DatabaseServiceProvider.php | grep -A 10 -B 5 "blueprintResolver"

echo -e "\n=== View full HasConnection trait ==="
cat src/Database/Connections/HasConnection.php

echo -e "\n=== Check new schema grammar implementations ==="
cat src/Database/Schema/Grammars/MySqlGrammar.php | head -50

echo -e "\n=== Check if tests are passing ==="
cat phpunit.xml.dist

echo -e "\n=== Review phpstan baseline changes ==="
echo "Total baseline issues before: 136"
echo "Total baseline issues after: 513"
echo "Net increase: 377 new baseline issues"

Length of output: 6790


🏁 Script executed:

#!/bin/bash

echo "=== Check CI/test status ==="
gh pr checks 207 --json name,conclusion,status

echo -e "\n=== View Migration.php to understand changes ==="
cat src/Database/Updates/Migration.php

echo -e "\n=== Check for any migration-related test failures ==="
fd -t f -e php . tests/ | xargs grep -l "Migration" | head -5

echo -e "\n=== Review key concerns from MySqlBasedGrammar ==="
cat src/Database/Schema/Grammars/Concerns/MySqlBasedGrammar.php | head -80

Length of output: 2808


🏁 Script executed:

#!/bin/bash

echo "=== Check CI/test status properly ==="
gh pr checks 207

echo -e "\n=== Count PHPStan issues by category ==="
echo "Analyzing phpstan-baseline.neon..."
grep -E "message:|identifier:" phpstan-baseline.neon | head -50

echo -e "\n=== Check what Laravel 12 changed in migrations ==="
# Let's see if there are any comments about migration issues
gh pr view 207 --json comments --jq '.comments[].body' | grep -i "migration" -A 3 -B 3

echo -e "\n=== Review new HasAttributes concern ==="
cat src/Database/Concerns/HasAttributes.php

Length of output: 7823


@LukeTowers I'll review this PR. Based on my analysis, here's what I found:

Overview

This is a massive undertaking upgrading from Laravel 9 to 12 with 2,538 additions and 854 deletions across ~100 files. The scope includes:

  • Laravel Framework: 9 → 12
  • Doctrine DBAL: 2 → 3
  • PHP: 8.1 → 8.2+
  • Major database layer refactoring

Critical Issues

🔴 Code Analysis Failing

The Code Analysis CI check is currently failing while all unit tests pass. This needs to be addressed before merge.

🟡 PHPStan Baseline Explosion

The PHPStan baseline grew from 136 to 513 issues (+377 new issues). This is concerning as it indicates:

  1. Type safety regression - Laravel 12 introduced stricter type hints that Storm's codebase doesn't satisfy
  2. Technical debt accumulation - Suppressing 377 issues rather than fixing them
  3. Potential runtime issues masked by baseline suppression

Examples of new baseline issues:

  • Return type mismatches in relation methods (newBelongsTo, newHasMany, etc.)
  • PHPDoc type covariance issues with $hidden, $fillable, $appends properties
  • Method signature incompatibilities with parent classes

🟡 Unresolved Migration System Concerns

From the comment chain, @mjauvin expressed:

"Wow, they really messed it up!"
"not sure I'm going to be able to fix this"

Question: What specific migration system issues were you encountering? The PR shows the Blueprint resolver was updated, but are there remaining concerns about:

  • Column modification behavior changes in Laravel 11/12?
  • The compileChange() workaround in MySqlBasedGrammar?
  • Other breaking changes in Laravel's migration system?

Architectural Changes

✅ Database Layer Refactoring

New PDO Drivers (for Doctrine v3 compatibility):

  • Winter\Storm\Database\PDO\Connection
  • Winter\Storm\Database\PDO\MySqlDriver
  • Winter\Storm\Database\PDO\PostgresDriver
  • Winter\Storm\Database\PDO\SQLiteDriver
  • Winter\Storm\Database\PDO\SqlServerDriver

Connection Refactoring:

  • Extracted HasConnection trait with common functionality
  • Base Connection class marked as deprecated with @deprecated comment
  • Added new MariaDbConnection for MariaDB-specific support

Schema Grammar Updates:

  • New MySqlBasedGrammar concern that restores Laravel 10 behavior where column attributes persist during changes
  • Comprehensive test coverage added for all grammar implementations

✅ Positive Aspects

  1. All functional tests passing on PHP 8.2, 8.3, 8.4 (Ubuntu & Windows)
  2. Clean trait extraction - HasConnection properly encapsulates shared functionality
  3. New test coverage - Grammar tests added for MySQL, MariaDB, PostgreSQL, SQLite, SQL Server
  4. HasAttributes concern - Properly restores $dates property behavior removed in Laravel 10
  5. Doctrine v3 integration - Proper compatibility layer implemented

Recommendations

Before Merge

  1. Fix Code Analysis failure - Review the failing PHPStan check
  2. Triage PHPStan baseline issues:
    • Classify which issues are acceptable vs. need fixing
    • Consider fixing critical type mismatches rather than suppressing
    • Document why specific issues are baselined
  3. Resolve migration concerns - Confirm the migration system works as expected
  4. Test real-world scenarios:
    • Column modifications on existing tables
    • Complex migrations with foreign keys
    • Schema introspection with Doctrine

Consider

  • Progressive typing: Instead of baselining all 377 issues, could some be fixed incrementally?
  • Documentation: Add migration guide for users upgrading Winter applications to Laravel 12
  • Deprecation path: The abstract Connection class has @deprecated but no removal timeline

Questions

  1. Is the Code Analysis failure related to the new baseline issues?
  2. What specific migration functionality were you unable to fix?
  3. Should the deprecated Connection class have a version number for removal?
  4. Are there any breaking changes for Winter CMS users?

The core architecture looks solid, but the PHPStan baseline growth and Code Analysis failure need attention.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 24

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/Network/Http.php (1)

553-553: Incorrect CURLOPT_SSL_VERIFYHOST value in verifySSL() method.

Line 553 sets CURLOPT_SSL_VERIFYHOST to true, which coerces to integer 1. According to PHP's cURL documentation, this option should be set to 2 (not 1 or true) to properly verify that the certificate's Common Name or Subject Alternate Name matches the hostname.

Using 1 is deprecated and may not perform proper hostname verification.

Apply this diff to fix the SSL host verification:

 public function verifySSL()
 {
     $this->setOption(CURLOPT_SSL_VERIFYPEER, true);
-    $this->setOption(CURLOPT_SSL_VERIFYHOST, true);
+    $this->setOption(CURLOPT_SSL_VERIFYHOST, 2);
     return $this;
 }
src/Database/MigrationServiceProvider.php (1)

12-14: Restore or replace dropped migration commands

  • src/Database/MigrationServiceProvider.php clears all Laravel migration commands, and no *Migrate*Command classes or registrations were found in the codebase.
  • Implement and register equivalent migration commands or revert to Laravel’s defaults in MigrationServiceProvider.
  • Document the new migration workflow for Winter CMS users and plugin developers.
src/Database/Connections/MySqlConnection.php (1)

70-79: Bug: floats bound as integers (loss of precision) and null/bool not handled

Same issue as MariaDbConnection: float values get truncated; handle null and booleans explicitly.

    public function bindValues($statement, $bindings)
    {
        foreach ($bindings as $key => $value) {
            $statement->bindValue(
                is_string($key) ? $key : $key + 1,
                $value,
-                is_int($value) || is_float($value) ? PDO::PARAM_INT : PDO::PARAM_STR
+                is_int($value) ? PDO::PARAM_INT
+                    : (is_bool($value) ? PDO::PARAM_BOOL
+                    : (is_null($value) ? PDO::PARAM_NULL
+                    : PDO::PARAM_STR))
            );
        }
    }

Consider extracting a shared bindValueType($value): int helper (or moving this into HasConnection) to avoid duplication.

♻️ Duplicate comments (1)
src/Database/Traits/Revisionable.php (1)

160-163: Guard access to $this->dates to avoid notices; use strict checks

Accessing an undefined $dates can trigger notices on some models. Add property_exists/is_array and strict in_array.

Apply:

-        if (in_array($attribute, $this->getDates()) || $this->isDateCastable($attribute) || in_array($attribute, $this->dates)) {
+        if (
+            in_array($attribute, $this->getDates(), true)
+            || $this->isDateCastable($attribute)
+            || (property_exists($this, 'dates') && is_array($this->dates) && in_array($attribute, $this->dates, true))
+        ) {
             return 'date';
         }

Longer-term, consider centralizing legacy $dates → $casts migration (or have getDates() include $dates) so callers don’t need to handle both.

🧹 Nitpick comments (48)
src/Foundation/Bootstrap/LoadConfiguration.php (1)

45-58: Replace the ignore flags with Env::get

Using // @phpstan-ignore-next-line twice suppresses legitimate checks and can hide future issues. Instead, pull in Illuminate\Support\Env (already present in Laravel 12) and call Env::get() so PHPStan understands the symbol:

+use Illuminate\Support\Env;
...
-        // @phpstan-ignore-next-line
-        return env('APP_ENV', array_get($config, 'default', 'production'));
+        return Env::get('APP_ENV', array_get($config, 'default', 'production'));
...
-        // @phpstan-ignore-next-line
-        $environment = env('APP_ENV');
+        $environment = Env::get('APP_ENV');

This keeps behavior identical while eliminating the need for ignores.

src/Foundation/Http/Kernel.php (1)

35-42: LGTM: Property rename aligns with Laravel 11+ requirements.

The property rename from $routeMiddleware to $middlewareAliases is correct and necessary for Laravel 12 compatibility. Laravel deprecated $routeMiddleware in v11 and replaced it with $middlewareAliases.

The middleware configuration is appropriate:

  • Commented middleware (auth, auth.basic, can, guest) are application-level concerns that Winter CMS applications should define themselves
  • Retained middleware (bindings, throttle) are framework-level routing infrastructure

Optional: Clean up commented lines

Consider removing the commented middleware lines entirely rather than keeping them as comments, since they represent application-level middleware that shouldn't be defined at the framework level:

 protected $middlewareAliases = [
-    // 'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
-    // 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
     'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
-    // 'can' => \Illuminate\Auth\Middleware\Authorize::class,
-    // 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
     'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
 ];

However, keeping the comments may serve as documentation for Winter CMS users showing which middleware aliases are available in Laravel but intentionally omitted from the framework base.

tests/Foundation/ProviderRepositoryTest.php (1)

49-49: Fix typos in method names.

Both method names contain the typo "Functionaliy" instead of "Functionality".

-    public function testOriginalFunctionaliy(): void
+    public function testOriginalFunctionality(): void
-    public function testWinterFunctionaliy(): void
+    public function testWinterFunctionality(): void

Also applies to: 67-67

src/Support/Facades/File.php (1)

47-47: Consider using the short form for consistency.

Line 47 uses the fully qualified name \Illuminate\Filesystem\FilesystemAdapter, while lines 57–58 use the short form FilesystemAdapter (now that the import is added). For consistency, consider updating line 47 to use the short form as well.

Apply this diff to use the short form:

-* @method static bool isLocalDisk(\Illuminate\Filesystem\FilesystemAdapter $disk)
+* @method static bool isLocalDisk(FilesystemAdapter $disk)
tests/Parse/YamlTest.php (1)

10-223: Consider addressing the skipped tests.

Multiple tests are currently skipped with TODO comments indicating a need to rethink YAML processing logic. While this technical debt predates the current PR, these skipped tests reduce coverage of the YAML parser functionality.

If you'd like, I can help draft a plan to address these TODOs or open an issue to track this improvement.

tests/Html/BlockBuilderTest.php (1)

7-7: Optional: Nullable type is defensively safe but not strictly necessary.

The ?BlockBuilder $Block = null declaration is defensive programming since setUp() always initializes $Block before any test method runs. The nullable type and default value don't cause issues but add slight verbosity. You could simplify to protected BlockBuilder $Block; if preferred, though the current approach is also acceptable.

tests/Support/EventFakeTest.php (1)

8-8: Optional: Nullable type is unnecessary here.

Since setUp() always initializes $faker before every test method (per PHPUnit's contract), the nullable type adds minimal defensive value. A non-nullable typed property without a default would suffice:

-    protected ?EventFake $faker = null;
+    protected EventFake $faker;

That said, the explicit null initialization is harmless and may satisfy stricter static analysis tools or coding standards in the upgraded environment.

tests/Halcyon/ValidationTraitTest.php (2)

3-6: LGTM: Valid PHPUnit 11 modernization pattern.

The ValidationTraitHost helper class correctly replaces the deprecated getMockForTrait() approach from older PHPUnit versions. This pattern is appropriate for testing traits in PHPUnit 11.

Optionally, consider adding a docblock to clarify the helper's purpose:

+/**
+ * Helper class to test the Validation trait.
+ */
 class ValidationTraitHost
 {
     use \Winter\Storm\Halcyon\Traits\Validation;
 }

12-12: Align trait testing with PHPUnit 11 test-double APIs
Replace new ValidationTraitHost() with $this->createStub(ValidationTraitHost::class) or use the MockBuilder fluent API ($this->getMockBuilder(ValidationTraitHost::class)->disableOriginalConstructor()->getMock()) to follow PHPUnit 11 recommendations.

src/Extension/ExtendableTrait.php (1)

550-563: Tighten param type to ReflectionClass and drop the phpstan ignore

Use a typed ReflectionClass parameter and an instanceof check to remove the need for the ignore and ensure $reflector is always a ReflectionClass.

-    protected function extensionGetParentClass(?object $instance = null)
+    protected function extensionGetParentClass(?ReflectionClass $instance = null)
     {
         // Shortcut to prevent infinite loops if the class extends Extendable.
         if ($this instanceof Extendable) {
             return false;
         }

         // Find if any parent uses the Extendable trait
-        if (!is_null($instance)) {
-            $reflector = $instance;
-        } else {
-            // @phpstan-ignore-next-line
-            $reflector = new ReflectionClass($this);
-        }
+        if ($instance instanceof ReflectionClass) {
+            $reflector = $instance;
+        } else {
+            /** @var ReflectionClass<object> $reflector */
+            $reflector = new ReflectionClass($this);
+        }
         $parent = $reflector->getParentClass();
phpstan.neon (1)

9-10: Avoid broad analysis downgrades; scope ignores

Global ignores degrade signal:

  • trait.unused: prefer path-specific baseline entries for known false-positives.
  • treatPhpDocTypesAsCertain: false reduces type precision; consider enabling later per-path if needed.

Add reportUnmatchedIgnoredErrors to prevent stale ignores.

Proposed tweaks:

 parameters:
   paths:
     - src
   level: 5
-  ignoreErrors:
-      - identifier: trait.unused
+  ignoreErrors:
+      # Consider moving specific trait.unused cases to baseline with path+count
   excludePaths:
     # Exclude PHP Parser files
     - src/Parse/PHP/ArrayFile.php
     - src/Foundation/Console/KeyGenerateCommand.php
     - src/Scaffold/GeneratorCommand.php
   disableSchemaScan: true
   databaseMigrationsPath:
     - src/Auth/Migrations
     - src/Database/Migrations
   treatPhpDocTypesAsCertain: false
+  reportUnmatchedIgnoredErrors: true

Please confirm Code Analysis passes with these settings or if trait.unused must remain global for now.

Also applies to: 20-20

.github/workflows/tests.yml (1)

37-39: Guard Composer token step to avoid empty auth writes on forks

On pull_request from forks, secrets are unavailable. Guarding prevents writing an empty token to global auth.

-      - name: Configure Composer GitHub token
-        run: composer config --global --auth github-oauth.github.com ${{ secrets.COMPOSER_GITHUB_TOKEN }}
+      - name: Configure Composer GitHub token
+        if: ${{ secrets.COMPOSER_GITHUB_TOKEN != '' }}
+        run: composer config --global --auth github-oauth.github.com ${{ secrets.COMPOSER_GITHUB_TOKEN }}
phpstan-baseline.neon (1)

1-1039: Consolidate and incrementally reduce phpstan baseline ignores

The current baseline masks many real mismatches (e.g. return.type, method.childParameterType, private Relation methods, property.notFound on ColumnDefinition/Fluent). Recommend:

  • Group ignores by identifier category (Relations, Grammar properties, Mailer, etc.) and tackle each in turn.
  • Introduce stubs or generics for dynamic Eloquent APIs to eliminate most method.notFound entries.
  • Keep ignore counts minimal and scoped to paths; annotate each block with rationale.
  • Open a follow-up issue to track and plan incremental baseline cleanup.

To verify baseline ignores locally:

#!/usr/bin/env bash
# Locate phpstan
PHPSTAN="./vendor/bin/phpstan"
[ ! -x "$PHPSTAN" ] && PHPSTAN="$(which phpstan 2>/dev/null || echo phpstan)"
$PHPSTAN analyse --no-progress --error-format=github || true
rg -oP 'identifier:\s*\K\S+' phpstan-baseline.neon | sort | uniq -c | sort -nr
tests/Database/ModelTest.php (1)

10-10: LGTM! Test base class standardized.

The migration to the fully-qualified \Winter\Storm\Tests\DbTestCase aligns with the broader test suite modernization effort.

Consider removing the now-redundant import at Line 8 since you're using the fully-qualified class name:

-use Winter\Storm\Tests\DbTestCase;
tests/Database/Traits/SimpleTreeTest.php (1)

9-9: LGTM! Test base class standardized.

The fully-qualified base class usage is consistent with the test suite modernization.

The import at Line 7 can be removed now that you're using the fully-qualified class name:

-use Winter\Storm\Tests\DbTestCase;
tests/Database/Traits/SortableTest.php (1)

7-7: LGTM! Test base class standardized.

The switch to fully-qualified \Winter\Storm\Tests\DbTestCase is consistent with the test suite refactor.

Consider removing the now-unused import at Line 5:

-use Winter\Storm\Tests\DbTestCase;
tests/Database/Traits/NestedTreeTest.php (1)

9-9: LGTM! Test base class standardized.

The fully-qualified base class aligns with the broader test infrastructure modernization.

The import at Line 7 is now redundant and can be removed:

-use Winter\Storm\Tests\DbTestCase;
tests/Database/Relations/AttachManyTest.php (1)

8-8: Remove unused import.

The import statement for DbTestCase is no longer needed since line 10 uses the fully-qualified class name in the extends clause.

Apply this diff to remove the redundant import:

-use Winter\Storm\Tests\DbTestCase;
tests/Database/Traits/DeferredBindingTest.php (1)

9-9: Remove unused import.

The import statement for DbTestCase is no longer needed since line 11 uses the fully-qualified class name in the extends clause.

Apply this diff to remove the redundant import:

-use Winter\Storm\Tests\DbTestCase;
tests/Database/Traits/SluggableTest.php (1)

6-6: Remove unused import.

The import statement for DbTestCase is no longer needed since line 8 uses the fully-qualified class name in the extends clause.

Apply this diff to remove the redundant import:

-use Winter\Storm\Tests\DbTestCase;
tests/Database/Relations/HasManyThroughTest.php (1)

10-10: Optional: Remove redundant import.

Since the class now extends the fully-qualified \Winter\Storm\Tests\DbTestCase on line 12, the import on line 10 is no longer needed.

Apply this diff to remove the redundant import:

-use Winter\Storm\Tests\DbTestCase;
tests/Database/Relations/DuplicateRelationTest.php (1)

9-9: Optional: Remove redundant import.

The import is no longer needed since line 11 uses the fully-qualified class name.

Apply this diff:

-use Winter\Storm\Tests\DbTestCase;
tests/fixtures/events/EventTest.php (1)

5-13: Drop TestCase inheritance and dummy code in EventTest fixture
In tests/fixtures/events/EventTest.php remove extends TestCase, the custom __construct override and the testDummy() method so that EventTest is a plain event payload class.

tests/Database/UpdaterTest.php (1)

5-14: Call parent::setUp() when extending the Winter test base

Switch to the FQ base class is fine, and nullable property is OK. Add parent::setUp() to ensure framework bootstrap runs.

 public function setUp(): void
 {
+    parent::setUp();
     include_once __DIR__.'/../fixtures/database/SampleClass.php';

     $this->updater = new Updater();
 }
tests/Support/MailFakeTest.php (1)

4-14: Good MailManager injection; add Mockery teardown

The manager mock + MailFake swap is correct. Ensure Mockery is closed to prevent leaks; either use the PHPUnit integration trait or close in tearDown.

Option A (preferred): use the integration trait.

 use Mockery as m;
+use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;

 class MailFakeTest extends \Winter\Storm\Tests\TestCase
 {
+    use MockeryPHPUnitIntegration;

Option B: close Mockery in tearDown (if you prefer not to use the trait).

protected function tearDown(): void
{
    m::close();
    parent::tearDown();
}

Also applies to: 20-22

tests/Halcyon/HalcyonModelTest.php (1)

10-26: Add parent::setUp() with new base test class

The new base class likely requires bootstrap. Insert parent::setUp() at the start of setUp().

 public function setUp(): void
 {
+    parent::setUp();
     include_once __DIR__.'/../fixtures/halcyon/models/Page.php';
     include_once __DIR__.'/../fixtures/halcyon/models/Menu.php';
     include_once __DIR__.'/../fixtures/halcyon/models/Content.php';
tests/Filesystem/FilesystemTest.php (1)

3-4: LGTM; consider adding return types to providers

Attribute migration and static providers look good. Optionally add : array return types to provider methods for clarity.

Example:

-    public static function provideSizesForSizeToBytes()
+    public static function provideSizesForSizeToBytes(): array

Also applies to: 6-7, 15-17, 24-35, 36-43, 44-58, 60-67, 68-80, 82-89, 90-100

src/Database/Schema/Grammars/Concerns/MySqlBasedGrammar.php (1)

11-23: Suppress PHPMD for intentionally unused $command

Signature must match Laravel’s interface; add suppression to avoid PHPMD noise.

Add to the docblock:

 /**
  * Compile a change column command into a series of SQL statements.
@@
  * @return array|string
  *
  * @throws \RuntimeException
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  */
tests/Database/Relations/HasOneThroughTest.php (1)

11-11: LGTM! Test base class standardization.

The change to use the fully qualified \Winter\Storm\Tests\DbTestCase base class is correct and aligns with the PR-wide test suite refactoring. This makes the base class reference explicit and avoids potential namespace resolution issues.

Optional refinement: The import statement on Line 9 (use Winter\Storm\Tests\DbTestCase;) is now redundant since the fully qualified name is used in the extends clause. Consider removing it for consistency.

tests/Database/Traits/NullableTest.php (1)

8-8: LGTM! Test base class correctly updated.

The fully qualified base class reference is correct and consistent with the PR's test suite standardization.

Optional refinement: The import on Line 6 (use Winter\Storm\Tests\DbTestCase;) is now redundant and could be removed.

tests/Database/Relations/AttachOneTest.php (1)

12-12: LGTM! Consistent with test suite refactoring.

The base class update to \Winter\Storm\Tests\DbTestCase is correct.

Optional refinement: Line 10's import statement is now redundant.

tests/Database/Relations/BelongsToManyTest.php (1)

13-13: LGTM! Base class update is correct.

The fully qualified \Winter\Storm\Tests\DbTestCase reference aligns with the PR's test standardization.

Optional refinement: The import statement on Line 11 is now redundant.

tests/Database/Concerns/HasRelationshipsTest.php (1)

24-24: LGTM! Test base class standardization complete.

The update to use \Winter\Storm\Tests\DbTestCase is correct and finalizes the test suite refactoring pattern seen across all reviewed files.

Optional refinement: Line 22's import statement is now redundant and could be cleaned up.

src/Database/PDO/SQLiteDriver.php (1)

12-18: Add : string return type to getName() in all PDO driver classes
Doctrine DBAL is at ^3.0 and PHP 8+ supports covariant return types. Update each src/Database/PDO/*Driver.php:

-    public function getName()
+    public function getName(): string
src/Database/PDO/SqlServerDriver.php (1)

18-23: Consider using the ConnectsToDatabase trait for consistency.

Unlike the MySQL and PostgreSQL drivers which use the ConnectsToDatabase trait, this driver reimplements the connection logic. This creates code duplication and pattern inconsistency.

If the wrapping with SqlServerConnection (instead of plain Connection) is necessary, consider documenting why this driver requires different handling. Otherwise, apply this refactor:

 class SqlServerDriver extends AbstractSQLServerDriver
 {
+    use ConnectsToDatabase;
+
-    /**
-     * Create a new database connection.
-     *
-     * @param  mixed[]  $params
-     * @param  string|null  $username
-     * @param  string|null  $password
-     * @param  mixed[]  $driverOptions
-     * @return \Winter\Storm\Database\PDO\SqlServerConnection
-     */
-    public function connect(array $params, $username = null, $password = null, array $driverOptions = [])
-    {
-        return new SqlServerConnection(
-            new Connection($params['pdo'])
-        );
-    }
-
     /**
      * {@inheritdoc}
      */

Note: You may need to verify whether SqlServerConnection provides necessary server-specific behavior that justifies the custom wrapper.

tests/Database/Schema/Grammars/PostgresSchemaGrammarTest.php (1)

6-10: Consistent class reference

You import GrammarTestCase but extend via FQCN. Minor style nit: extend the imported name for consistency.

-use Winter\Storm\Tests\GrammarTestCase;
+use Winter\Storm\Tests\GrammarTestCase;
 
-class PostgresSchemaGrammarTest extends \Winter\Storm\Tests\GrammarTestCase
+class PostgresSchemaGrammarTest extends GrammarTestCase
src/Database/Connectors/ConnectionFactory.php (1)

17-21: Outdated/misleading comment about “uncatchable” exception

This comment references an override “below” that no longer exists here (only createConnection is overridden). Consider updating or removing to avoid confusion.

src/Database/Schema/Grammars/PostgresGrammar.php (1)

25-26: Unused parameter ($command) — silence PHPMD or use it

To satisfy PHPMD without altering the signature, explicitly unset the parameter.

     public function compileChange(Blueprint $blueprint, Fluent $command)
     {
+        unset($command);

Alternatively, add: @SuppressWarnings(PHPMD.UnusedFormalParameter) to the method docblock. [Based on static analysis hints]

tests/GrammarTestCase.php (2)

13-15: Add type declarations for properties.

The properties lack type hints, reducing type safety and IDE support. Declare explicit types for $connection, $grammarClass, and $builderClass.

Apply this diff:

-    protected $connection = null;
-    protected $grammarClass = null;
-    protected $builderClass = null;
+    protected ?\Mockery\MockInterface $connection = null;
+    protected ?string $grammarClass = null;
+    protected ?string $builderClass = null;

21-29: Simplify the mock setup pattern.

The mock is created on lines 21-25, then immediately reassigned on lines 27-29. This can be consolidated into a single chain for better readability.

Apply this diff:

-        $this->connection = m::mock(Connection::class)
-            ->shouldReceive('getTablePrefix')->andReturn('')
-            ->shouldReceive('getServerVersion')->andReturn('3.35')
-            ->shouldReceive('scalar')->andReturn('')
-            ->getMock();
-
-        $this->connection = $this->connection
-            ->shouldReceive('getSchemaGrammar')->andReturn($this->getSchemaGrammar())
+        $this->connection = m::mock(Connection::class)
+            ->shouldReceive('getTablePrefix')->andReturn('')
+            ->shouldReceive('getServerVersion')->andReturn('3.35')
+            ->shouldReceive('scalar')->andReturn('')
+            ->shouldReceive('getSchemaGrammar')->andReturn($this->getSchemaGrammar())
             ->getMock();
src/Database/Schema/Grammars/SqlServerGrammar.php (2)

60-67: Only strip surrounding quotes; don’t remove inner quotes in defaults

preg_replace removes all single quotes, corrupting defaults like O'Connor. Strip only the outer pair.

Apply:

-    public function getDefaultValue($value)
+    public function getDefaultValue($value)
     {
-        if (is_string($value)) {
-            $value = preg_replace('#\'#', '', $value);
-        }
+        if (is_string($value) && strlen($value) >= 2 && $value[0] === "'" && substr($value, -1) === "'") {
+            $value = substr($value, 1, -1);
+        }
 
         return parent::getDefaultValue($value);
     }

25-58: Optionally drop empty statements from the change batch

Defensive cleanup to avoid executing empty statements if upstream returns an empty drop-default SQL.

-        return $changes;
+        return array_values(array_filter($changes));
src/Database/Connections/Connection.php (1)

5-8: Use a proper docblock and add deprecation details

Current comment isn’t parsed as a docblock. Include since/version and replacement for clarity.

-/*
- * @deprecated
- */
+/**
+ * @deprecated 1.3 Use the concrete driver connections (e.g. MySqlConnection, MariaDbConnection, PostgresConnection, SqlServerConnection)
+ *                + HasConnection trait instead. Planned removal in 2.0.
+ */
 abstract class Connection extends BaseConnection
src/Database/PDO/SqlServerConnection.php (1)

121-131: Ensure quote() never returns false; throw on failure

PDO::quote can return false; pass-through violates DBAL expectations. Throw instead, then apply the null-byte workaround.

-    public function quote($value, $type = ParameterType::STRING)
+    public function quote($value, $type = ParameterType::STRING)
     {
-        $val = $this->connection->quote($value, $type);
-
-        // Fix for a driver version terminating all values with null byte...
-        if (\is_string($val) && str_contains($val, "\0")) {
-            $val = \substr($val, 0, -1);
-        }
-
-        return $val;
+        $val = $this->connection->quote($value, $type);
+        if ($val === false) {
+            throw new \RuntimeException('Failed to quote value for SQL Server.');
+        }
+        // Fix for a driver version terminating all values with null byte...
+        if (str_contains($val, "\0")) {
+            $val = substr($val, 0, -1);
+        }
+        return $val;
     }
src/Database/PDO/Connection.php (1)

1-5: Optional: enable strict types for safer typing

Add strict_types to catch type issues earlier.

-<?php
+<?php
+declare(strict_types=1);
src/Database/Schema/Grammars/SQLiteGrammar.php (2)

151-158: Default value sanitization is too aggressive

Removing all single quotes changes valid literals. Trim only wrapping quotes.

     public function getDefaultValue($value)
     {
-        if (is_string($value)) {
-            $value = preg_replace('#\'#', '', $value);
-        }
+        if (is_string($value) && strlen($value) >= 2 && $value[0] === "'" && substr($value, -1) === "'") {
+            $value = substr($value, 1, -1);
+        }
 
         return parent::getDefaultValue($value);
     }

28-29: Silence PHPMD UnusedFormalParameter (or mark intent)

$command and $column are required by the signature but unused here.

Options:

  • Add suppression on the methods’ docblocks:
  • Or explicitly mark as unused:
     public function compileChange(Blueprint $blueprint, Fluent $command)
     {
+        unset($command);
         $autoIncrementColumn = null;
     protected function typeVarChar(Fluent $column)
     {
+        unset($column);
         return 'varchar';
     }

Also applies to: 166-169

src/Database/Connections/HasConnection.php (1)

151-157: Connection construction: prefer DriverManager or drop redundant 'driver' param

You already pass a Driver instance as arg 2; the 'driver' string in params is unnecessary and can confuse config resolution. Consider DriverManager::getConnection or omitting 'driver' from params.

-            $this->doctrineConnection = new DoctrineConnection(array_filter([
-                'pdo' => $this->getPdo(),
-                'dbname' => $this->getDatabaseName(),
-                'driver' => $driver->getName(),
-                'serverVersion' => $this->getConfig('server_version'),
-            ]), $driver);
+            $this->doctrineConnection = new DoctrineConnection(array_filter([
+                'pdo' => $this->getPdo(),
+                'dbname' => $this->getDatabaseName(),
+                'serverVersion' => $this->getConfig('server_version'),
+            ]), $driver);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between deb4304 and bb077c8.

📒 Files selected for processing (165)
  • .github/workflows/code-analysis.yaml (1 hunks)
  • .github/workflows/code-quality.yaml (0 hunks)
  • .github/workflows/tests.yml (3 hunks)
  • .gitignore (2 hunks)
  • composer.json (3 hunks)
  • phpstan-baseline.neon (1 hunks)
  • phpstan.neon (1 hunks)
  • phpunit.xml.dist (1 hunks)
  • src/Auth/Manager.php (1 hunks)
  • src/Auth/Models/Throttle.php (1 hunks)
  • src/Auth/Models/User.php (5 hunks)
  • src/Config/ConfigWriter.php (2 hunks)
  • src/Config/Repository.php (1 hunks)
  • src/Console/Traits/HandlesCleanup.php (1 hunks)
  • src/Database/Attach/File.php (2 hunks)
  • src/Database/Behaviors/Encryptable.php (1 hunks)
  • src/Database/Builder.php (4 hunks)
  • src/Database/Concerns/HasAttributes.php (1 hunks)
  • src/Database/Connections/Connection.php (1 hunks)
  • src/Database/Connections/HasConnection.php (1 hunks)
  • src/Database/Connections/MariaDbConnection.php (1 hunks)
  • src/Database/Connections/MySqlConnection.php (3 hunks)
  • src/Database/Connections/PostgresConnection.php (3 hunks)
  • src/Database/Connections/SQLiteConnection.php (3 hunks)
  • src/Database/Connections/SqlServerConnection.php (4 hunks)
  • src/Database/Connectors/ConnectionFactory.php (2 hunks)
  • src/Database/Connectors/SQLiteConnector.php (0 hunks)
  • src/Database/DatabaseServiceProvider.php (4 hunks)
  • src/Database/MigrationServiceProvider.php (1 hunks)
  • src/Database/Model.php (2 hunks)
  • src/Database/PDO/Concerns/ConnectsToDatabase.php (1 hunks)
  • src/Database/PDO/Connection.php (1 hunks)
  • src/Database/PDO/MySqlDriver.php (1 hunks)
  • src/Database/PDO/PostgresDriver.php (1 hunks)
  • src/Database/PDO/SQLiteDriver.php (1 hunks)
  • src/Database/PDO/SqlServerConnection.php (1 hunks)
  • src/Database/PDO/SqlServerDriver.php (1 hunks)
  • src/Database/Query/Grammars/MariaDbGrammar.php (1 hunks)
  • src/Database/Query/Grammars/MySqlGrammar.php (1 hunks)
  • src/Database/Query/Grammars/PostgresGrammar.php (1 hunks)
  • src/Database/QueryBuilder.php (2 hunks)
  • src/Database/Relations/Concerns/DeferOneOrMany.php (3 hunks)
  • src/Database/Schema/Grammars/Concerns/MySqlBasedGrammar.php (1 hunks)
  • src/Database/Schema/Grammars/MariaDbGrammar.php (1 hunks)
  • src/Database/Schema/Grammars/MySqlGrammar.php (1 hunks)
  • src/Database/Schema/Grammars/PostgresGrammar.php (1 hunks)
  • src/Database/Schema/Grammars/SQLiteGrammar.php (1 hunks)
  • src/Database/Schema/Grammars/SqlServerGrammar.php (1 hunks)
  • src/Database/Traits/Revisionable.php (1 hunks)
  • src/Extension/ExtendableTrait.php (1 hunks)
  • src/Flash/FlashBag.php (2 hunks)
  • src/Foundation/Application.php (4 hunks)
  • src/Foundation/Bootstrap/LoadConfiguration.php (2 hunks)
  • src/Foundation/Http/Kernel.php (1 hunks)
  • src/Foundation/Http/Middleware/CheckForMaintenanceMode.php (1 hunks)
  • src/Halcyon/Builder.php (1 hunks)
  • src/Halcyon/Datasource/DbDatasource.php (2 hunks)
  • src/Halcyon/MemoryCacheManager.php (1 hunks)
  • src/Halcyon/Model.php (1 hunks)
  • src/Halcyon/Processors/SectionParser.php (1 hunks)
  • src/Html/FormBuilder.php (2 hunks)
  • src/Http/Middleware/TrustHosts.php (1 hunks)
  • src/Mail/Mailer.php (1 hunks)
  • src/Network/Http.php (1 hunks)
  • src/Support/Facades/DB.php (1 hunks)
  • src/Support/Facades/File.php (1 hunks)
  • src/Support/Facades/Flash.php (1 hunks)
  • src/Support/Facades/Mail.php (2 hunks)
  • src/Support/Testing/MocksClassLoader.php (1 hunks)
  • src/Support/helpers-array.php (1 hunks)
  • src/Translation/FileLoader.php (2 hunks)
  • src/Translation/Translator.php (1 hunks)
  • tests/Config/ConfigTest.php (1 hunks)
  • tests/Config/ConfigWriterTest.php (1 hunks)
  • tests/Database/Attach/ResizerTest.php (1 hunks)
  • tests/Database/Behaviors/EncryptableBehaviorTest.php (1 hunks)
  • tests/Database/Concerns/HasRelationshipsTest.php (1 hunks)
  • tests/Database/DongleTest.php (1 hunks)
  • tests/Database/Factories/FactoryTest.php (1 hunks)
  • tests/Database/ModelTest.php (1 hunks)
  • tests/Database/MorphPivotTest.php (1 hunks)
  • tests/Database/QueryBuilderTest.php (4 hunks)
  • tests/Database/Relations/AttachManyTest.php (1 hunks)
  • tests/Database/Relations/AttachOneTest.php (1 hunks)
  • tests/Database/Relations/BelongsToManyTest.php (1 hunks)
  • tests/Database/Relations/BelongsToTest.php (1 hunks)
  • tests/Database/Relations/DuplicateRelationTest.php (1 hunks)
  • tests/Database/Relations/DynamicRelationTest.php (1 hunks)
  • tests/Database/Relations/HasManyTest.php (1 hunks)
  • tests/Database/Relations/HasManyThroughTest.php (1 hunks)
  • tests/Database/Relations/HasOneTest.php (1 hunks)
  • tests/Database/Relations/HasOneThroughTest.php (1 hunks)
  • tests/Database/Relations/MorphManyTest.php (1 hunks)
  • tests/Database/Relations/MorphOneTest.php (1 hunks)
  • tests/Database/Schema/Grammars/MySqlSchemaGrammarTest.php (1 hunks)
  • tests/Database/Schema/Grammars/PostgresSchemaGrammarTest.php (1 hunks)
  • tests/Database/Schema/Grammars/SQLiteSchemaGrammarTest.php (1 hunks)
  • tests/Database/Schema/Grammars/SqlServerSchemaGrammarTest.php (1 hunks)
  • tests/Database/Traits/ArraySourceTest.php (1 hunks)
  • tests/Database/Traits/DeferredBindingTest.php (1 hunks)
  • tests/Database/Traits/EncryptableTest.php (1 hunks)
  • tests/Database/Traits/HasSortableRelationsTest.php (1 hunks)
  • tests/Database/Traits/NestedTreeTest.php (1 hunks)
  • tests/Database/Traits/NullableTest.php (1 hunks)
  • tests/Database/Traits/PurgeableTraitTest.php (1 hunks)
  • tests/Database/Traits/RevisionableTest.php (1 hunks)
  • tests/Database/Traits/SimpleTreeTest.php (1 hunks)
  • tests/Database/Traits/SluggableTest.php (1 hunks)
  • tests/Database/Traits/SoftDeleteTest.php (1 hunks)
  • tests/Database/Traits/SortableTest.php (1 hunks)
  • tests/Database/Traits/ValidationTest.php (2 hunks)
  • tests/Database/UpdaterTest.php (1 hunks)
  • tests/DbTestCase.php (1 hunks)
  • tests/Events/DispatcherTest.php (1 hunks)
  • tests/Extension/ExtendableTest.php (1 hunks)
  • tests/Extension/ExtendableTraitTest.php (2 hunks)
  • tests/Extension/ExtensionTest.php (1 hunks)
  • tests/Filesystem/FilesystemTest.php (5 hunks)
  • tests/Filesystem/PathResolverTest.php (1 hunks)
  • tests/Foundation/ApplicationTest.php (1 hunks)
  • tests/Foundation/Http/Middleware/CheckForTrustedHostTest.php (2 hunks)
  • tests/Foundation/Http/Middleware/CheckForTrustedProxiesTest.php (2 hunks)
  • tests/Foundation/ProviderRepositoryTest.php (1 hunks)
  • tests/GrammarTestCase.php (1 hunks)
  • tests/Halcyon/DatasourceResolverTest.php (1 hunks)
  • tests/Halcyon/HalcyonModelTest.php (2 hunks)
  • tests/Halcyon/MemoryRepositoryTest.php (1 hunks)
  • tests/Halcyon/SectionParserTest.php (1 hunks)
  • tests/Halcyon/ValidationTraitTest.php (2 hunks)
  • tests/Html/BlockBuilderTest.php (1 hunks)
  • tests/Html/FormBuilderTest.php (12 hunks)
  • tests/Html/HtmlBuilderTest.php (1 hunks)
  • tests/Html/HtmlHelperTest.php (1 hunks)
  • tests/Mail/MailerTest.php (1 hunks)
  • tests/Network/HttpTest.php (1 hunks)
  • tests/Parse/ArrayFileTest.php (1 hunks)
  • tests/Parse/BracketTest.php (1 hunks)
  • tests/Parse/EnvFileTest.php (1 hunks)
  • tests/Parse/IniTest.php (1 hunks)
  • tests/Parse/MarkdownTest.php (4 hunks)
  • tests/Parse/SyntaxFieldParserTest.php (1 hunks)
  • tests/Parse/SyntaxParserTest.php (1 hunks)
  • tests/Parse/YamlTest.php (1 hunks)
  • tests/Router/RouterHelperTest.php (1 hunks)
  • tests/Router/RouterTest.php (1 hunks)
  • tests/Router/RoutingUrlGeneratorTest.php (2 hunks)
  • tests/Router/UrlGeneratorTest.php (1 hunks)
  • tests/Scaffold/ScaffoldBaseTest.php (1 hunks)
  • tests/Support/ArrTest.php (1 hunks)
  • tests/Support/ClassLoaderTest.php (1 hunks)
  • tests/Support/CountableTest.php (1 hunks)
  • tests/Support/EmitterTest.php (2 hunks)
  • tests/Support/EventFakeTest.php (1 hunks)
  • tests/Support/ExtensionAndEmitterSerializationTest.php (1 hunks)
  • tests/Support/HelpersTest.php (1 hunks)
  • tests/Support/MailFakeTest.php (1 hunks)
  • tests/Support/StrTest.php (1 hunks)
  • tests/Support/SvgTest.php (1 hunks)
  • tests/Translation/FileLoaderTest.php (2 hunks)
  • tests/Translation/TranslatorTest.php (4 hunks)
  • tests/Validation/EmailValidationTest.php (1 hunks)
  • tests/Validation/RuleObjectTest.php (1 hunks)
  • tests/bootstrap.php (0 hunks)
  • tests/fixtures/events/EventTest.php (1 hunks)
  • tests/stubs/Facades.stub (0 hunks)
💤 Files with no reviewable changes (4)
  • tests/bootstrap.php
  • .github/workflows/code-quality.yaml
  • tests/stubs/Facades.stub
  • src/Database/Connectors/SQLiteConnector.php
🧰 Additional context used
🪛 PHPMD (2.15.0)
src/Database/PDO/SqlServerDriver.php

18-18: Avoid unused parameters such as '$username'. (undefined)

(UnusedFormalParameter)


18-18: Avoid unused parameters such as '$password'. (undefined)

(UnusedFormalParameter)


18-18: Avoid unused parameters such as '$driverOptions'. (undefined)

(UnusedFormalParameter)

src/Database/Schema/Grammars/PostgresGrammar.php

25-25: Avoid unused parameters such as '$command'. (undefined)

(UnusedFormalParameter)

src/Database/Schema/Grammars/Concerns/MySqlBasedGrammar.php

24-24: Avoid unused parameters such as '$command'. (undefined)

(UnusedFormalParameter)

src/Translation/FileLoader.php

22-22: Avoid unused parameters such as '$output'. (undefined)

(UnusedFormalParameter)

src/Console/Traits/HandlesCleanup.php

46-46: Avoid unused parameters such as '$previousExitCode'. (undefined)

(UnusedFormalParameter)

src/Database/PDO/Concerns/ConnectsToDatabase.php

22-22: Avoid unused parameters such as '$username'. (undefined)

(UnusedFormalParameter)


22-22: Avoid unused parameters such as '$password'. (undefined)

(UnusedFormalParameter)


22-22: Avoid unused parameters such as '$driverOptions'. (undefined)

(UnusedFormalParameter)

src/Database/Schema/Grammars/SQLiteGrammar.php

28-28: Avoid unused parameters such as '$command'. (undefined)

(UnusedFormalParameter)


166-166: Avoid unused parameters such as '$column'. (undefined)

(UnusedFormalParameter)

Comment on lines +55 to +62
"phpunit/phpunit": "^11.0",
"squizlabs/php_codesniffer": "^3.2",
"php-parallel-lint/php-parallel-lint": "^1.0",
"meyfa/phpunit-assert-gd": "^3.0.0",
"dms/phpunit-arraysubset-asserts": "^0.5.0",
"larastan/larastan": "^2.8.1",
"orchestra/testbench": "^7.47.0"
"meyfa/phpunit-assert-gd": "^4.2",
"mockery/mockery": "^1.6|^2.0",
"dms/phpunit-arraysubset-asserts": "dev-add-phpunit-11-support",
"orchestra/testbench": "^10.0",
"larastan/larastan": "^3.6.0"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify dev-branch PHPUnit dependency stability.

The upgrade to PHPUnit ^11.0 is appropriate, but there's a critical stability concern:

Line 60 uses dms/phpunit-arraysubset-asserts with dev-add-phpunit-11-support from a fork (lines 64-69). This dev branch is not a stable, tagged release and could break without notice.

Verify whether this fork has been merged upstream or if a stable release is available:


🌐 Web query:

Is there a stable release of dms/phpunit-arraysubset-asserts that supports PHPUnit 11?

💡 Result:

No — there is no stable release that declares support for PHPUnit 11. The latest stable release is v0.5.0 (Jun 2023) and its composer constraint allows up to PHPUnit ^10.0. [1][2]

Sources:

  • Packagist: dms/phpunit-arraysubset-asserts (v0.5.0 requires phpunit ^4.8 … ^10.0). [1]
  • GitHub repository releases (latest v0.5.0, Jun 7, 2023). [2]

Pin or replace the dev-branch dependency in composer.json. There’s no stable dms/phpunit-arraysubset-asserts release supporting PHPUnit 11, so lock the “dev-add-phpunit-11-support” branch to a specific commit or vendor a tagged fork to avoid unexpected breaks.

🤖 Prompt for AI Agents
In composer.json around lines 55 to 62, the dev-branch dependency
"dms/phpunit-arraysubset-asserts": "dev-add-phpunit-11-support" must be pinned
to avoid breakage; replace the floating dev branch with a specific commit
reference (e.g. "dev-add-phpunit-11-support#<commit-hash>") or switch to an
alternative stable fork/tag and update composer.lock, or vendor the patched code
into the repo and point composer to the local path; after changing the
requirement run composer update for that package and commit composer.json and
composer.lock.

*/
public function attemptResetPassword($resetCode, $newPassword)
{
if ($this->checkResetPasswordCode($resetCode)) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify consistency with setPasswordAttribute().

The refactoring correctly updates checkPassword(), attemptResetPassword(), and getAuthPassword() to use the configurable password attribute name. However, setPasswordAttribute() (lines 318-329) remains hardcoded to the 'password' attribute:

public function setPasswordAttribute($value)
{
    if ($this->exists && empty($value)) {
        unset($this->attributes['password']);  // Still hardcoded
    }
    else {
        $this->attributes['password'] = $value;  // Still hardcoded
        $this->attributes['persist_code'] = null;
    }
}

If a subclass changes $authPasswordName to a different column (e.g., 'user_password'), the getter would read from 'user_password' but the setter would still write to 'password', creating an inconsistency.

Run the following script to check if there are any other password-related methods that might need similar updates:

Also applies to: 295-295, 626-626


🏁 Script executed:

#!/bin/bash
# Description: Find methods that directly access the password attribute to verify consistency

# Search for direct password attribute access patterns
rg -n --type=php -C 3 '\$this->password(?!\w)|\$this->attributes\[.password.\]' src/Auth/Models/User.php

Length of output: 274


🏁 Script executed:

#!/bin/bash
# Locate the setter and hardcoded password attribute references
rg -n --type=php 'function setPasswordAttribute' -C5 src/Auth/Models/User.php
rg -n --type=php "\$this->attributes\['password'\]" -C3 src/Auth/Models/User.php

Length of output: 297


🏁 Script executed:

#!/bin/bash
# Inspect getAuthPassword implementation for dynamic attribute usage
rg -n --type=php 'function getAuthPassword' -C10 src/Auth/Models/User.php

Length of output: 757


Update setPasswordAttribute() to use dynamic password attribute
In src/Auth/Models/User.php (lines 318–329), replace hardcoded ['password'] with the value of $this->getAuthPasswordName() for both unset and assignment so the setter matches the configurable getter.

🤖 Prompt for AI Agents
In src/Auth/Models/User.php around lines 318–329, the setPasswordAttribute
setter currently uses the hardcoded ['password'] key; change it to use a single
variable $passwordField = $this->getAuthPasswordName() and use that for both the
unset and the assignment (e.g. unset($this->$passwordField);
$this->$passwordField = Hash::make($value);), ensuring the setter uses the same
dynamic password attribute as getAuthPasswordName().

Comment on lines +70 to +79
public function bindValues($statement, $bindings)
{
foreach ($bindings as $key => $value) {
$statement->bindValue(
is_string($key) ? $key : $key + 1,
$value,
is_int($value) || is_float($value) ? PDO::PARAM_INT : PDO::PARAM_STR
);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bug: floats bound as integers (loss of precision) and null/bool not handled

Binding floats with PDO::PARAM_INT truncates decimals. Also explicitly handle null and booleans.

     public function bindValues($statement, $bindings)
     {
         foreach ($bindings as $key => $value) {
             $statement->bindValue(
                 is_string($key) ? $key : $key + 1,
                 $value,
-                is_int($value) || is_float($value) ? PDO::PARAM_INT : PDO::PARAM_STR
+                is_int($value) ? PDO::PARAM_INT
+                    : (is_bool($value) ? PDO::PARAM_BOOL
+                    : (is_null($value) ? PDO::PARAM_NULL
+                    : PDO::PARAM_STR))
             );
         }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function bindValues($statement, $bindings)
{
foreach ($bindings as $key => $value) {
$statement->bindValue(
is_string($key) ? $key : $key + 1,
$value,
is_int($value) || is_float($value) ? PDO::PARAM_INT : PDO::PARAM_STR
);
}
}
public function bindValues($statement, $bindings)
{
foreach ($bindings as $key => $value) {
$statement->bindValue(
is_string($key) ? $key : $key + 1,
$value,
is_int($value) ? PDO::PARAM_INT
: (is_bool($value) ? PDO::PARAM_BOOL
: (is_null($value) ? PDO::PARAM_NULL
: PDO::PARAM_STR))
);
}
}
🤖 Prompt for AI Agents
In src/Database/Connections/MariaDbConnection.php around lines 70 to 79, the
current bindValues treats floats as PDO::PARAM_INT (losing decimals) and doesn't
handle nulls or booleans; change the type selection so integers use
PDO::PARAM_INT, booleans use PDO::PARAM_BOOL, nulls use PDO::PARAM_NULL, and
everything else (including floats) uses PDO::PARAM_STR to preserve precision;
keep existing key handling (is_string($key) ? $key : $key + 1) and apply the new
conditional when calling $statement->bindValue.

*/
class Model extends EloquentModel implements ModelInterface
{
use Concerns\HasAttributes;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Confirm HasAttributes provides required methods used below

getAttribute() (Line 1070) calls $this->hasAttribute($key), and the model relies on getDates(). Ensure Concerns\HasAttributes defines these methods after their removal from Model.


🏁 Script executed:

#!/bin/bash
# Confirm methods exist in the trait
fd -a 'HasAttributes.php'
rg -nP -C2 'trait\s+HasAttributes\b|function\s+hasAttribute\s*\(|function\s+getDates\s*\(' $(fd -a 'HasAttributes.php')

Length of output: 266


Define missing hasAttribute in HasAttributes trait
getDates() is present, but hasAttribute() is not defined—calls to getAttribute() will break. Add hasAttribute() (or import it) into src/Database/Concerns/HasAttributes.php.

🤖 Prompt for AI Agents
In src/Database/Model.php around line 25, the Model imports
Concerns\HasAttributes but the HasAttributes trait in
src/Database/Concerns/HasAttributes.php is missing the hasAttribute() method
which breaks calls from getAttribute(); add a public hasAttribute(string $key):
bool method to that trait (or import the correct trait providing it) that
returns whether the given attribute/key exists (checking the internal attributes
array or attribute storage used by the trait), ensure the method signature
matches existing getAttribute usage, and run tests to verify no call sites
break.

Comment on lines +21 to +45
return collect($this->paths)
->reduce(function ($output, $path) use ($lines, $locale, $group, $namespace) {
$winterNamespace = str_replace('.', '/', $namespace);

$file = "{$this->path}/{$locale}/{$namespace}/{$group}.php";
// Look for a Winter-managed namespace
$file = "{$path}/{$locale}/{$winterNamespace}/{$group}.php";
if ($this->files->exists($file)) {
return array_replace_recursive($lines, $this->files->getRequire($file));
}

if ($this->files->exists($file)) {
return array_replace_recursive($lines, $this->files->getRequire($file));
}
// Look for a Winter-managed namespace with a Winter-formatted locale (xx-xx instead of xx_XX)
$dashLocale = str_replace('_', '-', strtolower($locale));
$dashFile = "{$path}/{$dashLocale}/{$winterNamespace}/{$group}.php";
if ($dashFile !== $file && $this->files->exists($dashFile)) {
return array_replace_recursive($lines, $this->files->getRequire($dashFile));
}

// Try "xx-xx" format
$locale = str_replace('_', '-', strtolower($locale));
// Look for a vendor-managed namespace
$file = "{$path}/vendor/{$namespace}/{$locale}/{$group}.php";
if ($this->files->exists($file)) {
return array_replace_recursive($lines, $this->files->getRequire($file));
}

if ("{$this->path}/{$locale}/{$namespace}/{$group}.php" !== $file) {
$file = "{$this->path}/{$locale}/{$namespace}/{$group}.php";

if ($this->files->exists($file)) {
return array_replace_recursive($lines, $this->files->getRequire($file));
}
}

return $lines;
return $lines;
}, []);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix accumulator handling in loadNamespaceOverrides.

The reducer ignores $output, so any later path that lacks the file resets to the original $lines, wiping out overrides loaded from earlier paths. In multi-path setups (common in Winter), overrides effectively never apply if subsequent paths don't hit, which is a regression from the previous single-path logic. Please carry the accumulator forward (initialised with $lines) when merging each match. Example fix:

-        return collect($this->paths)
-            ->reduce(function ($output, $path) use ($lines, $locale, $group, $namespace) {
+        return collect($this->paths)
+            ->reduce(function ($output, $path) use ($locale, $group, $namespace) {
                 $winterNamespace = str_replace('.', '/', $namespace);
 
                 // Look for a Winter-managed namespace
                 $file = "{$path}/{$locale}/{$winterNamespace}/{$group}.php";
                 if ($this->files->exists($file)) {
-                    return array_replace_recursive($lines, $this->files->getRequire($file));
+                    $output = array_replace_recursive($output, $this->files->getRequire($file));
                 }
 
                 // Look for a Winter-managed namespace with a Winter-formatted locale (xx-xx instead of xx_XX)
                 $dashLocale = str_replace('_', '-', strtolower($locale));
                 $dashFile = "{$path}/{$dashLocale}/{$winterNamespace}/{$group}.php";
                 if ($dashFile !== $file && $this->files->exists($dashFile)) {
-                    return array_replace_recursive($lines, $this->files->getRequire($dashFile));
+                    $output = array_replace_recursive($output, $this->files->getRequire($dashFile));
                 }
 
                 // Look for a vendor-managed namespace
                 $file = "{$path}/vendor/{$namespace}/{$locale}/{$group}.php";
                 if ($this->files->exists($file)) {
-                    return array_replace_recursive($lines, $this->files->getRequire($file));
+                    $output = array_replace_recursive($output, $this->files->getRequire($file));
                 }
 
-                return $lines;
-            }, []);
+                return $output;
+            }, $lines);
🧰 Tools
🪛 PHPMD (2.15.0)

22-22: Avoid unused parameters such as '$output'. (undefined)

(UnusedFormalParameter)

🤖 Prompt for AI Agents
In src/Translation/FileLoader.php around lines 21 to 45, the reducer is
initialized with [] and always returns $lines when no file is found, which
discards any previously accumulated overrides; change the reducer to initialize
with $lines and when merging a found file use array_replace_recursive($output,
$this->files->getRequire($file)) (and likewise for the dash-locale and vendor
checks) so the accumulator ($output) is carried forward instead of being reset.

Comment on lines +69 to +73
$dashLocale = str_replace('_', '-', strtolower($locale));
$dashFile = "{$path}/{$dashLocale}/{$group}.php";
if ($dashFile !== $file && $this->files->exists($dashFile)) {
return $this->files->getRequire($dashFile);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Preserve accumulated translations for dash-formatted locales.

In loadPaths, the dash-locale fallback currently returns the raw file contents, discarding anything merged into $output from earlier paths. Use array_replace_recursive($output, ...) here as well so previously loaded segments aren’t lost.

🤖 Prompt for AI Agents
In src/Translation/FileLoader.php around lines 69 to 73, the dash-locale
fallback currently returns the file contents directly and discards any
translations already merged into $output; change the return to merge the
dash-file array into $output using array_replace_recursive($output,
<dash-file-array>) so previously loaded segments are preserved, i.e. retrieve
the dash file contents and return array_replace_recursive($output, thatContents)
instead of returning the raw file contents.

@@ -0,0 +1,64 @@
<?php

namespace Tests\Database\Schema\Grammars;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix incorrect namespace declaration.

The namespace Tests\Database\Schema\Grammars is missing the Winter\Storm\ prefix, causing an inconsistency with other test classes in this PR (e.g., SqlServerSchemaGrammarTest uses Winter\Storm\Tests\Database\Schema\Grammars). This will cause autoloading and test discovery failures.

Apply this diff:

-namespace Tests\Database\Schema\Grammars;
+namespace Winter\Storm\Tests\Database\Schema\Grammars;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
namespace Tests\Database\Schema\Grammars;
namespace Winter\Storm\Tests\Database\Schema\Grammars;
🤖 Prompt for AI Agents
In tests/Database/Schema/Grammars/MySqlSchemaGrammarTest.php around line 3, the
namespace declaration currently is "namespace Tests\Database\Schema\Grammars;"
but should include the Winter\Storm\ prefix to match other tests; update the
namespace to "Winter\Storm\Tests\Database\Schema\Grammars" (adjust the line to
include the full prefix) so autoloading and test discovery align with the rest
of the test suite.

Comment on lines +51 to +52
$this->assertSame('alter column "name" null', $parts[1]);
$this->assertSame("alter column \"name\" set default 'admin'", $parts[2]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Invalid Postgres SQL in expectation ('null' is not a valid clause).

Postgres uses “drop not null” / “set not null”, not “… null”. The expected SQL should be “drop not null”. Also removes the extra space.

Apply this diff:

-        $this->assertSame('alter column "name"  null', $parts[1]);
+        $this->assertSame('alter column "name" drop not null', $parts[1]);
🤖 Prompt for AI Agents
In tests/Database/Schema/Grammars/PostgresSchemaGrammarTest.php around lines 51
to 52, the first expectation uses invalid Postgres SQL ("alter column \"name\" 
null"): replace it with the correct clause "alter column \"name\" drop not null"
(remove the extra space and use "drop not null"); leave the second expectation
unchanged.

Comment on lines +37 to +40
public function getConnection()
{
return ($this->connection)();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix incorrect mock invocation syntax.

Line 39 attempts to invoke the mock as a callable with ($this->connection)(), but Mockery mocks are not directly callable. This will cause a runtime error.

Apply this diff:

     public function getConnection()
     {
-        return ($this->connection)();
+        return $this->connection;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function getConnection()
{
return ($this->connection)();
}
public function getConnection()
{
return $this->connection;
}
🤖 Prompt for AI Agents
In tests/GrammarTestCase.php around lines 37 to 40, the getter incorrectly
attempts to invoke the mock with ($this->connection)(), which will error because
Mockery mocks are not callable; change the method to simply return the mock
object (return $this->connection;) or call the appropriate mock method if a
specific value is required, ensuring you do not use function-call syntax on the
mock.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/Halcyon/Builder.php (1)

161-175: Add null-safety check to lists() method at line 172.

The review comment is correct. With getFresh() returning array|null (line 282), passing the result directly to the Collection constructor without a null check creates a potential TypeError. The get() method properly handles this with $results ?: [] (line 331), but lists() lacks this protection. Apply the suggested defensive pattern:

-        $collection = new Collection($results);
+        $collection = new Collection($results ?: []);

This ensures consistency with the get() method and prevents errors when getFresh() returns null.

src/Database/Connections/MySqlConnection.php (1)

70-79: Bug: floats bound as integers (loss of precision) and null/bool not handled.

Binding floats with PDO::PARAM_INT truncates decimals. Also, null and boolean values should be explicitly handled with their respective PDO parameter types.

Apply this diff:

     public function bindValues($statement, $bindings)
     {
         foreach ($bindings as $key => $value) {
             $statement->bindValue(
                 is_string($key) ? $key : $key + 1,
                 $value,
-                is_int($value) || is_float($value) ? PDO::PARAM_INT : PDO::PARAM_STR
+                is_int($value) ? PDO::PARAM_INT
+                    : (is_bool($value) ? PDO::PARAM_BOOL
+                    : (is_null($value) ? PDO::PARAM_NULL
+                    : PDO::PARAM_STR))
             );
         }
     }
♻️ Duplicate comments (22)
src/Foundation/Http/Middleware/CheckForMaintenanceMode.php (1)

26-45: Re-throw non-503 HttpExceptions to preserve error handling.

Non-503 HttpExceptions are caught but not re-thrown, causing them to be silently swallowed and the method to return null. This breaks proper HTTP error handling for status codes like 404, 401, 403, etc.

Apply this diff to re-throw non-503 exceptions:

             }
+
+            throw $ex;
         }
     }
 }
src/Halcyon/MemoryCacheManager.php (1)

10-12: Verify that MemoryRepository constructor was added to prevent fatal error.

A past review flagged that MemoryRepository lacks a constructor accepting the $config parameter. The relevant code snippet confirms that MemoryRepository still has no __construct method defined. Without a proper constructor, calling new MemoryRepository($store, $config) will likely cause an ArgumentCountError if the parent Repository constructor only accepts one parameter.

Run the following script to verify whether:

  1. MemoryRepository now has a constructor accepting $config
  2. The parent Repository class constructor signature supports this change
#!/bin/bash
# Verify MemoryRepository and parent Repository constructor signatures

echo "=== Checking MemoryRepository constructor ==="
rg -n -A 10 "class MemoryRepository" src/Halcyon/MemoryRepository.php | rg -A 8 "__construct"

echo -e "\n=== Checking parent Repository class constructor ==="
# Repository is from Illuminate\Cache, check if Storm has a custom one or uses Laravel's
fd -t f "Repository.php" | xargs rg -l "namespace.*Cache" | head -5 | while read file; do
  echo "File: $file"
  rg -n -A 5 "class Repository" "$file" | rg -B 1 -A 5 "__construct"
done
src/Network/Http.php (1)

277-277: Technical improvement, but security concern persists.

Changing false to 0 is technically more correct, as CURLOPT_SSL_VERIFYHOST expects an integer value (0, 1, or 2). However, the underlying security issue flagged in the previous review remains: SSL host verification is still disabled by default, leaving all HTTP requests vulnerable to MITM attacks unless verifySSL() is explicitly called.

composer.json (1)

60-69: Pin the dev-branch dependency to a specific commit.

The dms/phpunit-arraysubset-asserts package uses an unstable dev branch that could break without notice. Since no stable release supports PHPUnit 11, pin the dependency to a specific commit hash.

Apply this pattern:

-        "dms/phpunit-arraysubset-asserts": "dev-add-phpunit-11-support",
+        "dms/phpunit-arraysubset-asserts": "dev-add-phpunit-11-support#abc1234",

Replace abc1234 with the actual commit hash from the fork.

src/Auth/Models/User.php (1)

316-325: Align setter with dynamic password column.

If $authPasswordName is overridden, this mutator still touches the hard-coded password key, so resets and assignments leave the configured column untouched. Please drive the unset/assignment through getAuthPasswordName() to keep the model consistent with the new configurability.

-        if ($this->exists && empty($value)) {
-            unset($this->attributes['password']);
-        } else {
-            $this->attributes['password'] = $value;
+        $passwordField = $this->getAuthPasswordName();
+
+        if ($this->exists && empty($value)) {
+            unset($this->attributes[$passwordField]);
+        } else {
+            $this->attributes[$passwordField] = $value;
src/Database/Relations/Concerns/DeferOneOrMany.php (1)

127-133: Don't coerce the deferred key expression to string.

Switching to ->getValue() forces the builder to treat the CAST(...) fragment as an identifier, so platforms like MySQL wrap it in backticks and break the deferred BelongsToMany / MorphToMany queries. Returning the raw Expression preserves the original behaviour.

-    protected function getWithDeferredQualifiedKeyName(): string
-    {
-        return $this->parent->getConnection()->raw(DbDongle::cast(
-            DbDongle::getTablePrefix() . $this->related->getQualifiedKeyName(),
-            'TEXT'
-        ))->getValue($this->parent->getGrammar());
-    }
+    protected function getWithDeferredQualifiedKeyName(): \Illuminate\Database\Query\Expression
+    {
+        return $this->parent->getConnection()->raw(DbDongle::cast(
+            DbDongle::getTablePrefix() . $this->related->getQualifiedKeyName(),
+            'TEXT'
+        ));
+    }
src/Translation/FileLoader.php (2)

21-45: Carry the reducer accumulator forward in loadNamespaceOverrides.

Returning $lines whenever a path misses wipes out overrides gathered from earlier paths, so multi-path namespace overrides silently fail. Seed the reducer with $lines and merge into $output each iteration instead of resetting it.

-        return collect($this->paths)
-            ->reduce(function ($output, $path) use ($lines, $locale, $group, $namespace) {
+        return collect($this->paths)
+            ->reduce(function ($output, $path) use ($locale, $group, $namespace) {
                 $winterNamespace = str_replace('.', '/', $namespace);
@@
-                return $lines;
-            }, []);
+                return $output;
+            }, $lines);

69-73: Merge dash-locale overrides instead of replacing the accumulator.

The dash-locale fallback currently returns the raw file contents, throwing away everything accumulated from prior paths. Merge the dash-locale array into $output so previously loaded segments survive.

-                if ($dashFile !== $file && $this->files->exists($dashFile)) {
-                    return $this->files->getRequire($dashFile);
+                if ($dashFile !== $file && $this->files->exists($dashFile)) {
+                    return array_replace_recursive($output, $this->files->getRequire($dashFile));
                 }
src/Database/PDO/Concerns/ConnectsToDatabase.php (1)

24-26: Fix the error message to reference Winter instead of Laravel.

The exception message incorrectly mentions "Laravel" but this is Winter CMS.

Apply this diff:

-        if (! isset($params['pdo']) || ! $params['pdo'] instanceof PDO) {
-            throw new InvalidArgumentException('Laravel requires the "pdo" property to be set and be a PDO instance.');
-        }
+        if (! isset($params['pdo']) || ! $params['pdo'] instanceof PDO) {
+            throw new InvalidArgumentException('Winter requires the "pdo" property to be set and be a PDO instance.');
+        }
src/Database/Model.php (1)

25-25: Confirm HasAttributes trait provides required methods.

The Model now uses Concerns\HasAttributes trait, but past review comments indicate that hasAttribute() method is missing from the trait. This method is called in getAttribute() at Line 1070 and will cause a fatal error if not defined.

tests/GrammarTestCase.php (1)

37-40: Fix incorrect mock invocation syntax.

Line 39 attempts to invoke the mock as a callable with ($this->connection)(), but Mockery mocks are not directly callable. This will cause a runtime error when getConnection() is called.

Apply this diff:

     public function getConnection()
     {
-        return ($this->connection)();
+        return $this->connection;
     }
src/Database/Schema/Grammars/PostgresGrammar.php (1)

63-70: Don't strip all single quotes from defaults; only trim surrounding quotes.

The preg_replace('#\'#', '', $value) call removes every single quote inside the string, which will corrupt values like "O'Connor" → "OConnor". Only remove wrapping quotes to avoid corrupting literals.

Apply this diff:

     public function getDefaultValue($value)
     {
-        if (is_string($value)) {
-            $value = preg_replace('#\'#', '', $value);
-        }
-
-        return parent::getDefaultValue($value);
+        if (is_string($value) && strlen($value) >= 2 && $value[0] === "'" && substr($value, -1) === "'") {
+            // Trim only surrounding single quotes to avoid double-quoting while preserving inner apostrophes
+            $value = substr($value, 1, -1);
+        }
+        return parent::getDefaultValue($value);
     }
src/Database/Schema/Grammars/Concerns/MySqlBasedGrammar.php (2)

36-36: Bug: Passing ColumnDefinition to wrap() will cause failure.

The wrap() method expects a string identifier, not a ColumnDefinition object. This will produce invalid SQL or fatal errors.

Apply this fix:

             $sql = sprintf(
                 '%s %s%s %s',
                 is_null($column->renameTo) ? 'modify' : 'change',
-                $this->wrap($column),
+                $this->wrap($column->name),
                 is_null($column->renameTo) ? '' : ' '.$this->wrap($column->renameTo),
                 $this->getType($column)
             );

59-66: Bug: Regex strips all quotes, corrupting values like "O'Reilly".

The current preg_replace('#\'#', '', $value) removes every single quote, including those inside the string. This corrupts default values containing apostrophes.

Apply this fix to only strip wrapping quotes:

     public function getDefaultValue($value)
     {
-        if (is_string($value)) {
-            $value = preg_replace('#\'#', '', $value);
+        if (is_string($value) && strlen($value) >= 2 && $value[0] === "'" && substr($value, -1) === "'") {
+            $value = substr($value, 1, -1);
         }
 
         return parent::getDefaultValue($value);
     }
tests/Database/Schema/Grammars/PostgresSchemaGrammarTest.php (1)

51-51: Bug: Invalid Postgres SQL syntax in test expectation.

Postgres does not support ... null as a clause. The correct syntax is drop not null to make a column nullable. The expectation also has an extra space.

Apply this fix:

-        $this->assertSame('alter column "name"  null', $parts[1]);
+        $this->assertSame('alter column "name" drop not null', $parts[1]);
tests/Database/Schema/Grammars/MySqlSchemaGrammarTest.php (1)

3-3: Fix incorrect namespace declaration.

The namespace is missing the Winter\Storm\ prefix, which will cause autoloading and test discovery failures. This is inconsistent with other test classes in the codebase.

Apply this diff:

-namespace Tests\Database\Schema\Grammars;
+namespace Winter\Storm\Tests\Database\Schema\Grammars;
src/Database/Schema/Grammars/SQLiteGrammar.php (1)

103-105: Bug: str_starts_with arguments reversed; sqlite_ indexes not filtered.

The arguments to str_starts_with are reversed, so internal sqlite_* indexes will never be filtered out. This can cause errors when recreating indexes.

Apply this diff:

-        $indexes = collect($indexes)->reject(fn ($index) => str_starts_with('sqlite_', $index->index))->map(
+        $indexes = collect($indexes)->reject(fn ($index) => str_starts_with($index->index, 'sqlite_'))->map(
             fn ($index) => $this->{'compile'.ucfirst($index->name)}($blueprint, $index)
         )->all();
src/Database/PDO/Connection.php (4)

42-53: Don't rely on assert; handle PDO::exec() false explicitly.

PDO::exec() can return false without throwing an exception. The current code uses assert() which is insufficient, and returning false violates the int return type.

Apply this diff:

     public function exec(string $statement): int
     {
         try {
             $result = $this->connection->exec($statement);
-
-            \assert($result !== false);
-
-            return $result;
+            if ($result === false) {
+                $errorInfo = $this->connection->errorInfo();
+                throw Exception::new(new PDOException($errorInfo[2] ?? 'PDO::exec() failed', 0));
+            }
+            return (int) $result;
         } catch (PDOException $exception) {
             throw Exception::new($exception);
         }
     }

61-70: Guard against PDO::prepare() returning false.

PDO::prepare() returns false on failure without throwing an exception. The current code can trigger a TypeError in createStatement().

Apply this diff:

     public function prepare(string $sql): StatementInterface
     {
         try {
-            return $this->createStatement(
-                $this->connection->prepare($sql)
-            );
+            $pdoStmt = $this->connection->prepare($sql);
+            if ($pdoStmt === false) {
+                $errorInfo = $this->connection->errorInfo();
+                throw Exception::new(new PDOException($errorInfo[2] ?? 'PDO::prepare() failed', 0));
+            }
+            return $this->createStatement($pdoStmt);
         } catch (PDOException $exception) {
             throw Exception::new($exception);
         }
     }

78-89: Guard against PDO::query() returning false.

PDO::query() can return false on failure. Avoid relying on assert() for production error handling.

Apply this diff:

     public function query(string $sql): ResultInterface
     {
         try {
             $stmt = $this->connection->query($sql);
-
-            \assert($stmt instanceof PDOStatement);
-
-            return new Result($stmt);
+            if ($stmt === false) {
+                $errorInfo = $this->connection->errorInfo();
+                throw Exception::new(new PDOException($errorInfo[2] ?? 'PDO::query() failed', 0));
+            }
+            return new Result($stmt);
         } catch (PDOException $exception) {
             throw Exception::new($exception);
         }
     }

158-161: quote() may return false; enforce string return.

PDO::quote() returns string|false. Returning false violates the declared return type.

Apply this diff:

     public function quote($input, $type = ParameterType::STRING)
     {
-        return $this->connection->quote($input, $type);
+        $quoted = $this->connection->quote($input, $type);
+        if ($quoted === false) {
+            $errorInfo = $this->connection->errorInfo();
+            throw Exception::new(new PDOException($errorInfo[2] ?? 'PDO::quote() failed', 0));
+        }
+        return $quoted;
     }
src/Database/Connections/MariaDbConnection.php (1)

70-79: Bug: floats bound as integers (loss of precision) and null/bool not handled.

Binding floats with PDO::PARAM_INT truncates decimals. Null and boolean values should also be explicitly handled with their respective PDO parameter types.

Apply this diff:

     public function bindValues($statement, $bindings)
     {
         foreach ($bindings as $key => $value) {
             $statement->bindValue(
                 is_string($key) ? $key : $key + 1,
                 $value,
-                is_int($value) || is_float($value) ? PDO::PARAM_INT : PDO::PARAM_STR
+                is_int($value) ? PDO::PARAM_INT
+                    : (is_bool($value) ? PDO::PARAM_BOOL
+                    : (is_null($value) ? PDO::PARAM_NULL
+                    : PDO::PARAM_STR))
             );
         }
     }
🧹 Nitpick comments (14)
tests/Scaffold/ScaffoldBaseTest.php (1)

23-29: Consider removing duplicate helper method.

The callProtectedMethod helper is already defined in the parent class \Winter\Storm\Tests\TestCase (see tests/TestCase.php lines 26-34) with an identical implementation. You can safely remove this duplicate and rely on the inherited method.

Apply this diff to remove the duplication:

-    protected static function callProtectedMethod($object, $name, $params = [])
-    {
-        $className = get_class($object);
-        $class = new ReflectionClass($className);
-        $method = $class->getMethod($name);
-        $method->setAccessible(true);
-        return $method->invokeArgs($object, $params);
-    }
-
src/Support/Facades/File.php (1)

47-47: Consider using unqualified class name for consistency.

Since the FilesystemAdapter import is now present (line 4), you could simplify the fully qualified reference here to match the style used in lines 57-58.

Apply this diff for consistency:

- * @method static bool isLocalDisk(\Illuminate\Filesystem\FilesystemAdapter $disk)
+ * @method static bool isLocalDisk(FilesystemAdapter $disk)
tests/Parse/YamlTest.php (1)

8-8: LGTM! Change correctly aligns with test-suite standardization.

The update to extend \Winter\Storm\Tests\TestCase is correct and consistent with the broader test-suite consolidation described in the PR objectives.

For improved readability and consistency with modern PHP conventions, consider adding a use statement at the top of the file instead of using the fully-qualified namespace in the class declaration:

 use Winter\Storm\Parse\Processor\YamlProcessor;
 use Symfony\Component\Yaml\Exception\ParseException;
 use Winter\Storm\Parse\Processor\Symfony3Processor;
 use Winter\Storm\Parse\Yaml as YamlParser;
+use Winter\Storm\Tests\TestCase;
 
-class YamlTest extends \Winter\Storm\Tests\TestCase
+class YamlTest extends TestCase
tests/Database/UpdaterTest.php (1)

7-7: Consider removing the nullable type since setUp() always initializes the property.

The nullable type and null default appear defensive but are functionally unnecessary since PHPUnit guarantees setUp() runs before each test method. The property is never actually null during test execution.

Consider simplifying to a non-nullable property without a default value:

-    protected ?Updater $updater = null;
+    protected Updater $updater;

This relies on setUp() to initialize the property (which it always does on line 13) and better expresses the invariant that the property is always initialized when tests run.

tests/Foundation/ProviderRepositoryTest.php (1)

49-49: Optional: Fix method name typos.

The method names testOriginalFunctionaliy and testWinterFunctionaliy contain typos (should be "Functionality"). While not new to this PR, consider correcting these for better code clarity.

Also applies to: 67-67

tests/Support/CountableTest.php (1)

2-2: LGTM! Explicit base class reference improves clarity.

The fully-qualified reference to \Winter\Storm\Tests\TestCase makes the inheritance chain explicit and aligns with the PR-wide standardization of test infrastructure.

Optional: Consider adding a namespace declaration.

The file lacks a namespace declaration. Adding one (e.g., namespace Winter\Storm\Tests\Support;) would follow modern PHP best practices and better organize the test suite, though this may be intentional for the current test structure.

src/Auth/Models/Preferences.php (2)

63-63: Modern PHP syntax: explicit constructor call.

Using new static() with explicit parentheses aligns with modern PHP style guidelines and improves consistency.


116-117: Modern PHP syntax: short array destructuring.

The migration from list(...) to [...] syntax adopts PHP 7.1+ short array destructuring, improving code readability and consistency with modern PHP practices.

Also applies to: 177-177

src/Database/Traits/Revisionable.php (1)

160-161: Consider removing the redundant $this->dates check.

The condition checks in_array($attribute, $this->getDates()) followed by a direct check of in_array($attribute, $this->dates). Since getDates() already merges $this->dates with timestamp columns (as seen in HasAttributes.php lines 20-32), the final check appears redundant.

If the redundant check is for backward compatibility, consider documenting why it's necessary. Otherwise, simplify to:

-        if (in_array($attribute, $this->getDates()) || $this->isDateCastable($attribute) || in_array($attribute, $this->dates)) {
+        if (in_array($attribute, $this->getDates()) || $this->isDateCastable($attribute)) {
             return 'date';
         }
tests/Database/Relations/DynamicRelationTest.php (1)

11-11: LGTM: Test inheritance standardized.

The fully qualified base class name standardizes test inheritance across the suite. The import on line 9 makes this redundant, but the explicit qualification doesn't hurt.

Optionally, you could remove the import on line 9 or use the imported alias:

-class DynamicRelationTest extends \Winter\Storm\Tests\DbTestCase
+class DynamicRelationTest extends DbTestCase
tests/Database/Relations/AttachManyTest.php (1)

10-10: LGTM: Test inheritance standardized.

The fully qualified base class name aligns with the standardization effort across test files. As with DynamicRelationTest, the import on line 8 makes this redundant but not incorrect.

Optionally simplify by using the imported alias:

-class AttachManyTest extends \Winter\Storm\Tests\DbTestCase
+class AttachManyTest extends DbTestCase
src/Database/Connections/Connection.php (1)

5-7: Add a target removal version to the deprecation notice.

The @deprecated tag should include a version number indicating when this class will be removed to help users plan migrations.

Apply this diff:

 /*
- * @deprecated
+ * @deprecated Will be removed in v2.0. Extend driver-specific connections (MySqlConnection, PostgresConnection, etc.) instead.
  */
src/Database/Schema/Grammars/Concerns/MySqlBasedGrammar.php (1)

24-24: Unused parameter $command can be removed.

The static analysis tool correctly identifies that $command is not used in this method.

However, retaining it may be intentional for interface consistency with other grammar implementations. If not needed, consider removing it:

-    public function compileChange(Blueprint $blueprint, Fluent $command)
+    public function compileChange(Blueprint $blueprint)
src/Database/Connections/HasConnection.php (1)

16-16: Consider adding native type hints to properties.

Both $doctrineConnection and $doctrineTypeMappings rely on PHPDoc for type information. Adding native PHP type hints would improve static analysis and reduce the PHPStan issues mentioned in the PR context.

Apply this diff:

-    protected $doctrineConnection = null;
+    protected ?DoctrineConnection $doctrineConnection = null;
-    protected $doctrineTypeMappings = [];
+    protected array $doctrineTypeMappings = [];

Also applies to: 23-23

Comment on lines +96 to +109
public function isDoctrineAvailable()
{
return class_exists('Doctrine\DBAL\Connection');
}

/**
* Indicates whether native alter operations will be used when dropping or renaming columns, even if Doctrine DBAL is installed.
*
* @return bool
*/
public function usingNativeSchemaOperations()
{
return ! $this->isDoctrineAvailable();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Clarify whether Doctrine DBAL is optional or required.

The runtime checks in isDoctrineAvailable() and usingNativeSchemaOperations() suggest Doctrine DBAL is optional, but the PR context indicates a required upgrade from Doctrine DBAL 2 to 3. Additionally, registerDoctrineType() (line 180-184) throws a RuntimeException if Doctrine is unavailable, further suggesting it's expected to be present.

If Doctrine DBAL is now a required dependency, these runtime checks and the exception in registerDoctrineType() may be unnecessary.

Run the following script to check if Doctrine DBAL is declared as a required or optional dependency:


🏁 Script executed:

#!/bin/bash
# Description: Check composer.json for Doctrine DBAL dependency status

# Check if doctrine/dbal is in require or require-dev
if [ -f "composer.json" ]; then
  echo "=== Checking Doctrine DBAL dependency status ==="
  cat composer.json | jq -r '.require["doctrine/dbal"] // "NOT IN REQUIRE"'
  echo "---"
  cat composer.json | jq -r '."require-dev"["doctrine/dbal"] // "NOT IN REQUIRE-DEV"'
else
  echo "composer.json not found"
fi

Length of output: 76


Let me verify how these methods are used and check the registerDoctrineType() method context:


🏁 Script executed:

#!/bin/bash
# Find usages of isDoctrineAvailable and usingNativeSchemaOperations
echo "=== Usages of isDoctrineAvailable() ==="
rg -n "isDoctrineAvailable\(\)" --type=php

echo ""
echo "=== Usages of usingNativeSchemaOperations() ==="
rg -n "usingNativeSchemaOperations\(\)" --type=php

echo ""
echo "=== registerDoctrineType() method (lines 180-184) ==="
rg -n -A8 "registerDoctrineType" --type=php

Length of output: 1903


Let me check if usingNativeSchemaOperations() is actually being used anywhere:


🏁 Script executed:

#!/bin/bash
# Search more broadly for usage of usingNativeSchemaOperations
echo "=== All references to usingNativeSchemaOperations ==="
rg -n "usingNativeSchemaOperations" --type=php

echo ""
echo "=== Check in test files ==="
rg -n "usingNativeSchemaOperations" --type=php -g '**test*'

echo ""
echo "=== Check in all files (any extension) ==="
rg -n "usingNativeSchemaOperations"

Length of output: 320


Remove or update dead code related to optional Doctrine support.

Doctrine DBAL is a required dependency (^3.0 in composer.json), but the code maintains runtime checks suggesting it's optional:

  • usingNativeSchemaOperations() is never called anywhere in the codebase and always returns false (dead code).
  • registerDoctrineType() throws a RuntimeException if Doctrine is unavailable, but since Doctrine is required, this exception path is unreachable.
  • These checks contradict the actual dependency status and should be removed or the logic clarified.

Comment on lines +134 to +138
// Doctrine v2 expects one parameter while v3 expects two. 2nd will be ignored on v2...
return $this->getDoctrineDriver()->getSchemaManager(
$connection,
$connection->getDatabasePlatform()
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify Doctrine DBAL v2/v3 compatibility approach.

The comment states "Doctrine v2 expects one parameter while v3 expects two. 2nd will be ignored on v2..." However, passing two arguments to a method expecting one in PHP will typically result in an ArgumentCountError rather than silently ignoring the extra parameter.

If this PR only targets Doctrine DBAL v3 (as suggested by the PR context of "Doctrine DBAL 2 → 3"), the comment may be outdated and the code is correct. If both v2 and v3 support is required, you'll need conditional logic based on the Doctrine version.

Run the following script to check the Doctrine DBAL version constraints and verify the compatibility approach:


🏁 Script executed:

#!/bin/bash
# Description: Check Doctrine DBAL version constraint and verify getSchemaManager signature

echo "=== Doctrine DBAL Version Constraint ==="
if [ -f "composer.json" ]; then
  cat composer.json | jq -r '.require["doctrine/dbal"] // .["require-dev"]["doctrine/dbal"] // "NOT FOUND"'
fi

echo ""
echo "=== Checking for Doctrine v2/v3 compatibility code ==="
# Search for version checks or conditional logic related to Doctrine
rg -n "doctrine.*version|DBAL.*version" --type php -i

echo ""
echo "=== Check if there are any other Doctrine compatibility comments ==="
rg -n "doctrine v[0-9]|dbal v[0-9]" --type php -i

Length of output: 314


Update or remove the misleading Doctrine v2 compatibility comment.

The codebase targets only Doctrine DBAL v3 (^3.0 in composer.json), with no conditional logic for v2 support. The comment at line 134 suggesting v2 compatibility is outdated and misleading.

Recommended fix: Replace the comment with clarification that the two-parameter call is required for Doctrine DBAL v3:

        // Doctrine DBAL v3 requires SchemaManager to accept Connection and AbstractPlatform
        return $this->getDoctrineDriver()->getSchemaManager(
            $connection,
            $connection->getDatabasePlatform()
        );
🤖 Prompt for AI Agents
In src/Database/Connections/HasConnection.php around lines 134 to 138, the
existing comment claiming Doctrine v2 compatibility is outdated and misleading;
update it to state that Doctrine DBAL v3 requires passing both the Connection
and its AbstractPlatform, then replace the old comment with a concise
clarification that the two-parameter call is required for Doctrine DBAL v3 while
leaving the return call unchanged.

Comment on lines +186 to +189
if (! Type::hasType($name)) {
Type::getTypeRegistry()
->register($name, is_string($class) ? new $class() : $class);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Doctrine DBAL v3-only API may break v2 compatibility.

The code uses Type::getTypeRegistry()->register() which is Doctrine DBAL v3 API. In Doctrine DBAL v2, the equivalent was Type::addType($name, $class).

Combined with the v2/v3 compatibility comment in getDoctrineSchemaManager() (line 134), there's inconsistency in the compatibility approach. If this PR drops Doctrine DBAL v2 support entirely (as suggested by "Doctrine DBAL 2 → 3" in the PR description), this is fine. However, if v2 support must be maintained, you'll need conditional logic here.


🏁 Script executed:

#!/bin/bash
# Description: Search for other uses of Doctrine v3-specific APIs that might break v2 compatibility

echo "=== Searching for Type::getTypeRegistry() usage (v3 only) ==="
rg -n "Type::getTypeRegistry|getTypeRegistry\(\)" --type php

echo ""
echo "=== Searching for Type::addType() usage (v2 API) ==="
rg -n "Type::addType|addType\(" --type php

echo ""
echo "=== Check composer.lock for installed Doctrine version ==="
if [ -f "composer.lock" ]; then
  cat composer.lock | jq -r '.packages[] | select(.name == "doctrine/dbal") | {name: .name, version: .version}'
fi

Length of output: 258


I need to gather more information about the Doctrine version requirements and the PR context.

Now I need to verify the composer.json to confirm the Doctrine version requirements:


---

🏁 Script executed:

```shell
#!/bin/bash
# Check composer.json for Doctrine DBAL version requirements
echo "=== composer.json (if exists) ==="
if [ -f "composer.json" ]; then
  cat composer.json | jq '.require, .["require-dev"]' 2>/dev/null || grep -i doctrine composer.json
else
  echo "composer.json not found"
fi

echo ""
echo "=== Check HasConnection.php for version-related comments ==="
if [ -f "src/Database/Connections/HasConnection.php" ]; then
  cat src/Database/Connections/HasConnection.php | grep -i -E "(doctrine|dbal|v2|v3|version|compatibility|support)" | head -20
fi

Length of output: 119


Let me check for documentation files that might clarify the Doctrine version strategy:


---

🏁 Script executed:

```shell
#!/bin/bash
# Check for documentation files mentioning Doctrine version
echo "=== Checking README.md and similar ==="
fd -e md -e txt | head -20 | xargs grep -l -i "doctrine\|dbal\|v2\|v3" 2>/dev/null || echo "No docs found or no matches"

echo ""
echo "=== Check if composer.json exists elsewhere ==="
fd "composer.json" -o "composer.lock" 2>/dev/null

echo ""
echo "=== Check file context around line 186-189 in HasConnection.php ==="
if [ -f "src/Database/Connections/HasConnection.php" ]; then
  sed -n '170,200p' src/Database/Connections/HasConnection.php
fi

Length of output: 893


Let me check the getDoctrineSchemaManager() method at line 134 to verify the claimed compatibility comment:


---

🏁 Script executed:

```shell
#!/bin/bash
# Check lines around 134 in HasConnection.php for the claimed compatibility comment
if [ -f "src/Database/Connections/HasConnection.php" ]; then
  echo "=== Lines 120-150 around line 134 ==="
  sed -n '120,150p' src/Database/Connections/HasConnection.php
fi

Length of output: 943


Now I have the critical context! Let me verify whether there's any intent stated about dropping v2 support:


---

🏁 Script executed:

```shell
#!/bin/bash
# Search for any conditional logic or version checks in the codebase
echo "=== Search for version detection or compatibility checks ==="
rg -n "isDoctrineAvailable|getDoctrineDriver|version" --type php src/ | head -15

echo ""
echo "=== Search for TODO/FIXME about v2/v3 ==="
rg -n "TODO|FIXME" --type php src/Database/Connections/HasConnection.php

Length of output: 1549


Fix Doctrine DBAL v2 compatibility or explicitly drop v2 support.

The code uses Type::getTypeRegistry()->register() (Doctrine DBAL v3+ only), but the codebase already demonstrates v2/v3 compatibility efforts elsewhere—specifically in getDoctrineSchemaManager() at lines 136-137, which includes a comment acknowledging both versions.

This creates an inconsistency: the schema manager supports both versions, but custom type registration breaks on v2. Either:

  • Add conditional logic here to use Type::addType() for v2 and Type::getTypeRegistry()->register() for v3
  • Or confirm v2 support is being dropped project-wide and remove the compatibility comment from getDoctrineSchemaManager()
🤖 Prompt for AI Agents
In src/Database/Connections/HasConnection.php around lines 186-189, the custom
type registration uses Type::getTypeRegistry()->register() which is DBAL
v3-only; update to detect DBAL version and use Type::addType() for v2 and
Type::getTypeRegistry()->register() for v3. Specifically, if
Type::getTypeRegistry method exists use the current register($name, $instance)
flow (instantiating when $class is a string); otherwise call
Type::addType($name, $className) where $className is the class string (if $class
is an object use get_class($class)), and keep the existing Type::hasType check
to avoid double registration.

$sql = sprintf(
'alter table %s alter column %s %s',
$this->wrapTable($blueprint),
$this->wrap($column),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bug: Passing ColumnDefinition to wrap() will cause failure.

The wrap() method expects a string identifier (column name), not a ColumnDefinition object. This will produce invalid SQL.

Apply this fix:

             $sql = sprintf(
                 'alter table %s alter column %s %s',
                 $this->wrapTable($blueprint),
-                $this->wrap($column),
+                $this->wrap($column->name),
                 $this->getType($column)
             );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$this->wrap($column),
$this->wrap($column->name),
🤖 Prompt for AI Agents
In src/Database/Schema/Grammars/SqlServerGrammar.php around line 37, the code
passes a ColumnDefinition object into $this->wrap(), which expects a string
identifier; replace that argument with the column's name string (e.g.
$column->name or $column->getName() depending on the ColumnDefinition API) so
wrap() receives the actual column name and produces valid SQL.

Comment on lines +60 to +67
public function getDefaultValue($value)
{
if (is_string($value)) {
$value = preg_replace('#\'#', '', $value);
}

return parent::getDefaultValue($value);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Bug: Regex strips all quotes, corrupting values like "O'Reilly".

The preg_replace('#\'#', '', $value) removes every single quote in the string, corrupting default values that contain apostrophes.

Apply this fix to only strip the wrapping quotes:

     public function getDefaultValue($value)
     {
-        if (is_string($value)) {
-            $value = preg_replace('#\'#', '', $value);
+        if (is_string($value) && strlen($value) >= 2 && $value[0] === "'" && substr($value, -1) === "'") {
+            $value = substr($value, 1, -1);
         }
 
         return parent::getDefaultValue($value);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function getDefaultValue($value)
{
if (is_string($value)) {
$value = preg_replace('#\'#', '', $value);
}
return parent::getDefaultValue($value);
}
public function getDefaultValue($value)
{
if (is_string($value) && strlen($value) >= 2 && $value[0] === "'" && substr($value, -1) === "'") {
$value = substr($value, 1, -1);
}
return parent::getDefaultValue($value);
}
🤖 Prompt for AI Agents
In src/Database/Schema/Grammars/SqlServerGrammar.php around lines 60 to 67, the
current preg_replace strips every apostrophe inside the string (corrupting
values like "O'Reilly"); change the logic to only remove wrapping single quotes:
detect if the first and last characters are single quotes (or use a regex that
matches only leading and trailing single quotes, e.g. ^'(.*)'$) and strip those
so inner apostrophes are preserved, handling empty quoted strings correctly.

Comment on lines +71 to 77
* @param string $path
* @return string
*/
public function publicPath()
public function publicPath($path = '')
{
return $this->basePath;
return $this->joinPaths($this->basePath, $path);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify publicPath implementation aligns with Winter CMS directory structure.

The method signature correctly matches Laravel 12's API. However, the implementation uses $this->basePath directly rather than a dedicated public path property. In standard Laravel applications, the public path typically points to a public subdirectory (e.g., basePath('public')), but Winter CMS may use the base path as its public directory.

Run the following script to verify the expected public directory structure:


🏁 Script executed:

#!/bin/bash
# Description: Check if Winter CMS uses a separate 'public' directory or if public path equals base path

# Check for documentation or configuration mentioning public directory structure
rg -n -C3 "public.*path|publicPath" --type=php -g '!vendor/**' -g '!tests/**'

# Look for index.php or public-facing entry points
fd -t f "index.php" -E vendor -E node_modules -x echo "Found index.php at: {}"

# Check if there's a 'public' directory in typical Winter CMS structure
fd -t d "^public$" -d 2 -E vendor -E node_modules

Length of output: 16957


Now I need to search the codebase to see if there's a $publicPath property or if Winter CMS actually uses basePath as the public path.

publicPath method implementation does not match Winter CMS directory structure.

The publicPath method should return the path to the public/web directory, but the current implementation returns $this->joinPaths($this->basePath, $path), which returns the base path, not the public directory. Winter CMS creates a separate public directory in the project root, so publicPath() should append 'public' to the base path.

Fix required: Change line 76 to: return $this->joinPaths($this->basePath, 'public', $path);

🤖 Prompt for AI Agents
In src/Foundation/Application.php around lines 71 to 77, the publicPath() method
currently returns the base path instead of the project's public/web directory;
update the return to join the base path with the public directory and the
optional $path (i.e., call joinPaths($this->basePath, 'public', $path)) so
publicPath() correctly points to the public folder and still handles an empty
$path gracefully.

Comment on lines +10 to +13
public function testDummy()
{
$this->assertTrue(true, true);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix the assertion call signature.

The second parameter to assertTrue() should be a failure message string, not a boolean. Line 12 passes true as the message parameter.

Apply this diff:

-        $this->assertTrue(true, true);
+        $this->assertTrue(true);

Or if a message is desired:

-        $this->assertTrue(true, true);
+        $this->assertTrue(true, 'Dummy assertion should pass');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function testDummy()
{
$this->assertTrue(true, true);
}
public function testDummy()
{
$this->assertTrue(true);
}
🤖 Prompt for AI Agents
In tests/fixtures/events/EventTest.php around lines 10 to 13 the call
$this->assertTrue(true, true) uses a boolean as the second argument but
PHPUnit::assertTrue expects the second parameter to be a string message; fix by
removing the second argument so it reads $this->assertTrue(true) or, if you need
a message, replace the second argument with a string like
$this->assertTrue(true, 'optional failure message').

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/Filesystem/FilesystemTest.php (1)

10-13: Call the parent setup so the Testbench app boots

Skipping parent::setUp(); means the Winter TestCase never initializes the Testbench application, so any code that expects the container/config to exist will break under Laravel 12’s harness. Please invoke the parent before creating the Filesystem instance.

     public function setUp(): void
     {
+        parent::setUp();
+
         $this->filesystem = new Filesystem();
     }
♻️ Duplicate comments (19)
src/Foundation/Http/Middleware/CheckForMaintenanceMode.php (1)

26-45: Re-throw non-503 HttpExceptions to avoid silent suppression.

This issue was already flagged in the previous review but remains unresolved. The catch block captures all HttpException instances but only handles status 503. Non-503 HTTP errors (400, 401, 404, 500, etc.) are silently suppressed, causing the middleware to return null and breaking the expected error-handling flow.

Apply the fix suggested in the previous review:

             return Response::make(View::make($view, [
                 'message'           => $ex->getMessage()
             ]), 503);
         }
+
+        throw $ex;
     }
 }
src/Halcyon/MemoryCacheManager.php (1)

10-12: Critical: Constructor signature mismatch with MemoryRepository.

This change passes a $config parameter to MemoryRepository, but the class does not define a constructor that accepts this parameter. This will cause a fatal error at runtime when the repository is instantiated.

As noted in the previous review, MemoryRepository needs a constructor defined to accept the $config parameter. The suggested fix from the prior review remains valid:

public function __construct(Store $store, array $config = [])
{
    parent::__construct($store);
    // Store or handle $config as needed
}

Verify whether MemoryRepository has been updated to accept this parameter:

#!/bin/bash
# Check if MemoryRepository constructor accepts $config parameter
file="src/Halcyon/MemoryRepository.php"
echo "=== Checking constructor in $file ==="
rg -n "function __construct" "$file" -A5
src/Support/helpers-array.php (1)

303-306: Implementation correct for Laravel 12; breaking change already flagged.

The implementation correctly calls Arr::shuffle($array) without a seed parameter, aligning with Laravel 12's API. The breaking change (removal of the $seed parameter) was comprehensively covered in the previous review, including verification scripts and migration guidance.

src/Network/Http.php (1)

276-277: Critical: SSL verification remains disabled.

Changing CURLOPT_SSL_VERIFYHOST from false to 0 doesn't improve security—both values disable host verification. For proper SSL verification, CURLOPT_SSL_VERIFYHOST should be set to 2 (verify that the certificate's Common Name or Subject Alternative Name matches the requested hostname).

As noted in the previous review, the current default makes all HTTPS requests vulnerable to man-in-the-middle attacks unless users explicitly call verifySSL().

Consider enabling SSL verification by default:

-        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
-        curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
+        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true);
+        curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2);

Then introduce a noVerifySSL() method for explicit opt-out:

/**
 * Disable SSL verification (use only for development/testing)
 */
public function noVerifySSL()
{
    $this->setOption(CURLOPT_SSL_VERIFYPEER, false);
    $this->setOption(CURLOPT_SSL_VERIFYHOST, 0);
    return $this;
}
src/Database/PDO/Connection.php (4)

42-53: Don't rely on assert; handle PDO::exec() false explicitly

exec() can return false without throwing. Returning false violates the int return type and hides the error.


61-70: Guard against PDO::prepare() returning false

prepare() returns false on failure (no exception). The current code can trigger a TypeError in createStatement().


78-89: Guard against PDO::query() returning false

query() can return false. Avoid relying on assert().


158-161: quote() may return false; enforce string return

PDO::quote() returns string|false. Returning false violates the declared behavior.

composer.json (1)

60-60: Dev-branch dependency remains unpinned

The dependency on dev-add-phpunit-11-support is still a floating dev branch, which can break without notice.

src/Database/PDO/Concerns/ConnectsToDatabase.php (1)

24-26: Correct the exception branding.

The guard still throws “Laravel requires…”, which is inaccurate for Winter CMS. Please update the message so diagnostics reference Winter instead.

-        if (! isset($params['pdo']) || ! $params['pdo'] instanceof PDO) {
-            throw new InvalidArgumentException('Laravel requires the "pdo" property to be set and be a PDO instance.');
-        }
+        if (! isset($params['pdo']) || ! $params['pdo'] instanceof PDO) {
+            throw new InvalidArgumentException('Winter requires the "pdo" property to be set and be a PDO instance.');
+        }
tests/Database/Schema/Grammars/PostgresSchemaGrammarTest.php (1)

49-52: Fix the expected nullable clause.

Postgres uses DROP NOT NULL to make a column nullable; 'alter column "name" null' isn’t valid SQL and will never match the grammar output. Please assert against drop not null.

-        $this->assertSame('alter column "name"  null', $parts[1]);
+        $this->assertSame('alter column "name" drop not null', $parts[1]);
tests/GrammarTestCase.php (1)

37-40: Return the mock directly.

($this->connection)() invokes the mock as a callable and will crash at runtime. Just return the mock instance (as previously requested) so consumers get the configured Connection.

     public function getConnection()
     {
-        return ($this->connection)();
+        return $this->connection;
     }
src/Database/Schema/Grammars/PostgresGrammar.php (1)

63-69: Trim only wrapping quotes when normalizing defaults.

preg_replace('#\'#', '', $value) deletes every apostrophe, so a default like 'O'Connor' becomes OConnor, corrupting schema diffs. Only strip matching outer quotes instead of all occurrences.

-        if (is_string($value)) {
-            $value = preg_replace('#\'#', '', $value);
-        }
+        if (is_string($value) && strlen($value) >= 2 && $value[0] === "'" && substr($value, -1) === "'") {
+            $value = substr($value, 1, -1);
+        }
src/Database/Schema/Grammars/Concerns/MySqlBasedGrammar.php (2)

33-38: Fix ColumnDefinition wrapping in ALTER statements.

wrap() expects an identifier string; passing the ColumnDefinition instance triggers a fatal conversion error when compileChange() runs. Use the column name (and only wrap renameTo when present) so the ALTER statement can be generated.

-            $sql = sprintf(
-                '%s %s%s %s',
-                is_null($column->renameTo) ? 'modify' : 'change',
-                $this->wrap($column),
-                is_null($column->renameTo) ? '' : ' '.$this->wrap($column->renameTo),
-                $this->getType($column)
-            );
+            $sql = sprintf(
+                '%s %s%s %s',
+                is_null($column->renameTo) ? 'modify' : 'change',
+                $this->wrap($column->name),
+                is_null($column->renameTo) ? '' : ' ' . $this->wrap($column->renameTo),
+                $this->getType($column)
+            );

59-63: Preserve inner quotes in default values.

preg_replace('#\'#', '', $value) removes every single quote, so defaults like O'Reilly become OReilly. Strip only a single pair of wrapping quotes before delegating to the parent.

-        if (is_string($value)) {
-            $value = preg_replace('#\'#', '', $value);
-        }
+        if (is_string($value) && strlen($value) >= 2 && $value[0] === "'" && substr($value, -1) === "'") {
+            $value = substr($value, 1, -1);
+        }
src/Database/Relations/Concerns/DeferOneOrMany.php (1)

127-133: Return the raw expression for deferred key lookups.

By coercing the raw CAST expression to string (and typing the method as string), the query builder now wraps it as an identifier in orWhereIn/whereNotIn, producing invalid SQL. Restore the previous behaviour by returning the Expression instance itself.

-    protected function getWithDeferredQualifiedKeyName(): string
-    {
-        return $this->parent->getConnection()->raw(DbDongle::cast(
-            DbDongle::getTablePrefix() . $this->related->getQualifiedKeyName(),
-            'TEXT'
-        ))->getValue($this->parent->getGrammar());
-    }
+    protected function getWithDeferredQualifiedKeyName()
+    {
+        return $this->parent->getConnection()->raw(DbDongle::cast(
+            DbDongle::getTablePrefix() . $this->related->getQualifiedKeyName(),
+            'TEXT'
+        ));
+    }
src/Auth/Models/User.php (1)

326-338: Keep the setter in sync with the configurable password column.

The new $authPasswordName hook is great, but setPasswordAttribute still hardcodes 'password', so changing the column name writes the wrong attribute and leaves the real password un-hashed. Use getAuthPasswordName() inside the mutator so reads/writes stay aligned.

-        if ($this->exists && empty($value)) {
-            unset($this->attributes['password']);
-        } else {
-            $this->attributes['password'] = $value;
+        $passwordField = $this->getAuthPasswordName();
+
+        if ($this->exists && empty($value)) {
+            unset($this->attributes[$passwordField]);
+        } else {
+            $this->attributes[$passwordField] = $value;
             // Password has changed, log out all users
             $this->attributes['persist_code'] = null;
         }
tests/Database/Schema/Grammars/MySqlSchemaGrammarTest.php (1)

3-3: Fix namespace to match test autoloading.

This test class sits under tests/Database/... just like the others, which all live in Winter\Storm\Tests\…. Leaving the namespace as Tests\… breaks PSR-4 resolution and PHPUnit discovery, so the suite will never execute these assertions. Please restore the Winter\Storm\Tests\Database\Schema\Grammars prefix so the test runner can load it.

-namespace Tests\Database\Schema\Grammars;
+namespace Winter\Storm\Tests\Database\Schema\Grammars;
src/Database/Connections/MariaDbConnection.php (1)

70-78: Bind values with the correct PDO types.

Binding floats as PDO::PARAM_INT truncates decimals, and null / boolean values fall through to PDO::PARAM_STR. That leads to incorrect data writes and unexpected SQL semantics. Mirror PDO expectations by branching on is_null, is_bool, is_int, and is_float separately.

-            $statement->bindValue(
-                is_string($key) ? $key : $key + 1,
-                $value,
-                is_int($value) || is_float($value) ? PDO::PARAM_INT : PDO::PARAM_STR
-            );
+            $statement->bindValue(
+                is_string($key) ? $key : $key + 1,
+                $value,
+                match (true) {
+                    is_null($value) => PDO::PARAM_NULL,
+                    is_bool($value) => PDO::PARAM_BOOL,
+                    is_int($value) => PDO::PARAM_INT,
+                    default => PDO::PARAM_STR,
+                }
+            );
🧹 Nitpick comments (16)
tests/Database/Traits/DeferredBindingTest.php (1)

9-11: Remove redundant import statement.

The use statement on line 9 imports DbTestCase, but line 11 uses the fully-qualified \Winter\Storm\Tests\DbTestCase. Either remove the import or use the unqualified name in the class declaration.

Apply this diff to remove the redundant import:

 namespace Winter\Storm\Tests\Database\Traits;
 
 use Winter\Storm\Database\Models\DeferredBinding;
 use Winter\Storm\Database\Model;
 use Winter\Storm\Tests\Database\Fixtures\Post;
 use Winter\Storm\Tests\Database\Fixtures\Author;
-use Winter\Storm\Tests\DbTestCase;
 
 class DeferredBindingTest extends \Winter\Storm\Tests\DbTestCase
 {
src/Support/Facades/File.php (1)

47-47: Consider using the short name for consistency.

Now that FilesystemAdapter is imported at line 4, this line can use the short name instead of the fully qualified namespace to match the style used in lines 57-58.

Apply this diff:

-* @method static bool isLocalDisk(\Illuminate\Filesystem\FilesystemAdapter $disk)
+* @method static bool isLocalDisk(FilesystemAdapter $disk)
tests/Database/Traits/SimpleTreeTest.php (1)

7-9: Consider removing the redundant import or using the imported alias.

Since line 9 now uses the fully qualified class name \Winter\Storm\Tests\DbTestCase, the import statement on line 7 is unused. For consistency, either remove the import or use the alias DbTestCase in the class declaration.

Option 1: Remove the unused import

-use Winter\Storm\Tests\DbTestCase;
 
 class SimpleTreeTest extends \Winter\Storm\Tests\DbTestCase

Option 2: Use the imported alias

 class SimpleTreeTest extends \Winter\Storm\Tests\DbTestCase
+class SimpleTreeTest extends DbTestCase

Note: If the fully qualified naming pattern is intentional across the PR for clarity or namespace consistency, then Option 1 would be preferred.

tests/Parse/SyntaxFieldParserTest.php (1)

379-404: Consider removing duplicate helper methods.

These helper methods (callProtectedMethod, getProtectedProperty, setProtectedProperty) duplicate functionality already available in the parent \Winter\Storm\Tests\TestCase class. The parent's callProtectedMethod is visible in the code snippets and serves the same purpose.

You can remove lines 379-404 and rely on the parent class implementation:

-    //
-    // Helpers
-    //
-
-    protected static function callProtectedMethod($object, $name, $params = [])
-    {
-        $className = get_class($object);
-        $class = new ReflectionClass($className);
-        $method = $class->getMethod($name);
-        $method->setAccessible(true);
-        return $method->invokeArgs($object, $params);
-    }
-
-    public static function getProtectedProperty($object, $name)
-    {
-        $className = get_class($object);
-        $class = new ReflectionClass($className);
-        $property = $class->getProperty($name);
-        $property->setAccessible(true);
-        return $property->getValue($object);
-    }
-
-    public static function setProtectedProperty($object, $name, $value)
-    {
-        $className = get_class($object);
-        $class = new ReflectionClass($className);
-        $property = $class->getProperty($name);
-        $property->setAccessible(true);
-        return $property->setValue($object, $value);
-    }
tests/Validation/RuleObjectTest.php (1)

8-8: LGTM! Consider adding a use statement for consistency.

The fully-qualified base class reference is correct and aligns with the PR-wide namespace refactoring for Laravel 12 compatibility.

For style consistency with the other imports (lines 3-6), you could optionally add a use statement at the top:

 use Illuminate\Filesystem\Filesystem;
 use Winter\Storm\Translation\FileLoader;
 use Winter\Storm\Translation\Translator;
 use Winter\Storm\Validation\Factory;
+use Winter\Storm\Tests\TestCase;

-class RuleObjectTest extends \Winter\Storm\Tests\TestCase
+class RuleObjectTest extends TestCase
tests/Foundation/ApplicationTest.php (1)

6-6: Consider using a use statement for cleaner code.

The fully-qualified namespace \Winter\Storm\Tests\TestCase is explicit and correct, but PHP convention typically favors importing the class with a use statement at the top of the file for improved readability.

Apply this diff if you prefer the conventional approach:

+use Winter\Storm\Tests\TestCase;
+
-class ApplicationTest extends \Winter\Storm\Tests\TestCase
+class ApplicationTest extends TestCase

Note: If the FQN usage is part of an intentional pattern across the test suite refactor, feel free to keep it as-is.

tests/Halcyon/ValidationTraitTest.php (1)

3-6: Consider applying the named class pattern to tests/Database/Traits/ValidationTest.php for consistency.

The script confirmed that only one other test file uses the anonymous class pattern: tests/Database/Traits/ValidationTest.php (line 125). Since the named class approach is better suited for static analysis tools (relevant to the PR's PHPStan concerns), applying the same pattern to that file would improve consistency across trait tests.

src/Foundation/Bootstrap/LoadConfiguration.php (1)

45-46: Consider investigating PHPStan concerns with env() calls.

Both suppressions relate to env() usage. The PHPStan baseline has grown significantly (136→513 issues); investigating and fixing the root type issues would be preferable to suppressing them.

Also applies to: 56-57

src/Database/Traits/Revisionable.php (1)

160-161: Remove the redundant $this->dates check from line 161.

The condition in_array($attribute, $this->getDates()) already covers this case. getDates() returns either $this->dates directly (when timestamps are disabled) or array_merge($this->dates, $defaults) (when enabled), so checking in_array($attribute, $this->dates) separately is redundant.

Simplify to:

if (in_array($attribute, $this->getDates()) || $this->isDateCastable($attribute)) {
tests/Database/Traits/SortableTest.php (1)

5-7: Remove redundant import or use the imported alias consistently.

The file imports Winter\Storm\Tests\DbTestCase on line 5 but then uses the fully-qualified name \Winter\Storm\Tests\DbTestCase on line 7. Either remove the import and keep the fully-qualified reference, or use the imported alias DbTestCase.

Apply this diff to use the import alias:

-class SortableTest extends \Winter\Storm\Tests\DbTestCase
+class SortableTest extends DbTestCase

Or remove the unused import:

-use Winter\Storm\Tests\DbTestCase;
-
-class SortableTest extends \Winter\Storm\Tests\DbTestCase
+class SortableTest extends \Winter\Storm\Tests\DbTestCase
tests/Parse/ArrayFileTest.php (1)

5-5: Remove redundant import or use the imported alias consistently.

Similar to other test files in this PR, this file would benefit from consistency. The test class extends the fully-qualified \Winter\Storm\Tests\TestCase but there's no explicit import shown. Ensure consistency across the codebase.

src/Database/Builder.php (1)

85-95: Consider adding Expression handling in exact mode.

The searchWhereInternal method now handles Expression instances in lines 107-109 for 'any'/'all' modes, but the 'exact' mode (lines 85-95) doesn't include similar handling. If a column is passed as an Expression in exact mode, it won't be properly resolved.

Consider adding Expression handling in the exact mode block:

         if ($mode === 'exact') {
             $this->where(function (Builder $query) use ($columns, $term) {
                 foreach ($columns as $field) {
                     if (!strlen($term)) {
                         continue;
                     }
+                    if ($field instanceof Expression) {
+                        $field = $field->getValue($query->getQuery()->getGrammar());
+                    }
                     $fieldSql = $this->query->raw(sprintf("lower(%s)", DbDongle::cast($field, 'text')));
                     $termSql = '%' . trim(mb_strtolower($term)) . '%';
                     $query->orWhere($fieldSql, 'LIKE', $termSql);
                 }
             }, null, null, $boolean);
tests/Database/Relations/DuplicateRelationTest.php (1)

9-11: Consider removing the redundant import.

Since the class extends the fully-qualified \Winter\Storm\Tests\DbTestCase (line 11), the import statement on line 9 is no longer necessary and could be removed for cleaner code.

Apply this diff to remove the redundant import:

-use Winter\Storm\Tests\DbTestCase;
-
-class DuplicateRelationTest extends \Winter\Storm\Tests\DbTestCase
+class DuplicateRelationTest extends \Winter\Storm\Tests\DbTestCase
tests/Database/Relations/DynamicRelationTest.php (1)

9-11: Consider removing the redundant import.

The import statement on line 9 is redundant since the class extends clause uses the fully-qualified class name. This pattern appears across multiple test files in this PR.

Apply this diff:

-use Winter\Storm\Tests\DbTestCase;
-
-class DynamicRelationTest extends \Winter\Storm\Tests\DbTestCase
+class DynamicRelationTest extends \Winter\Storm\Tests\DbTestCase
tests/Database/Relations/BelongsToTest.php (1)

8-10: Consider removing the redundant import.

The import statement on line 8 is unnecessary since the extends clause on line 10 uses the fully-qualified class name.

Apply this diff:

-use Winter\Storm\Tests\DbTestCase;
-
-class BelongsToTest extends \Winter\Storm\Tests\DbTestCase
+class BelongsToTest extends \Winter\Storm\Tests\DbTestCase
tests/Database/Traits/RevisionableTest.php (1)

7-9: Consider removing the redundant import.

The import on line 7 is unnecessary since the extends clause uses the fully-qualified class name. This redundancy appears across multiple test files in this PR.

Apply this diff:

-use Winter\Storm\Tests\DbTestCase;
-
-class RevisionableTest extends \Winter\Storm\Tests\DbTestCase
+class RevisionableTest extends \Winter\Storm\Tests\DbTestCase

$ast = $parser->parse($contents);
} catch (Error $e) {
throw new SystemException($e);
throw new SystemException($e);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix type mismatch in SystemException constructor.

The Error object is passed directly to SystemException, but the constructor expects a string message as the first parameter and calls HtmlBuilder::clean($message) on it. This will cause a type error at runtime.

Apply this diff to fix the type mismatch:

-            throw new SystemException($e);
+            throw new SystemException($e->getMessage(), $e->getCode(), $e);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
throw new SystemException($e);
throw new SystemException($e->getMessage(), $e->getCode(), $e);
🤖 Prompt for AI Agents
In src/Config/ConfigWriter.php around line 37, the code throws the Error object
directly into SystemException which expects a string message; change the throw
to pass the error message string (e.g. $e->getMessage()) as the first parameter
and, if SystemException supports a previous exception parameter, pass the
original $e as the second parameter so HtmlBuilder::clean receives a string and
the original error is preserved.

* @return int|false The exit code to return or false to continue the normal execution
*/
public function handleSignal(int $signal, /* int|false $previousExitCode = 0 */)
public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Verify unused parameter is required for interface compliance.

The $previousExitCode parameter is never used in the method body. If this signature is required by the SignalableCommandInterface or parent class contract, the parameter should remain. Otherwise, consider removing it.

Run the following script to check if this method signature matches a parent or interface requirement:


🏁 Script executed:

#!/bin/bash
# Check if handleSignal is defined in any parent class or interface

# Search for handleSignal method definitions in interfaces and parent classes
rg -n --type=php -A3 'function handleSignal' -g '!HandlesCleanup.php'

# Search for SignalableCommandInterface
rg -n --type=php -A10 'interface SignalableCommandInterface'

Length of output: 0


I need to search more broadly to find the interface definition and understand the class context. Let me try a different approach:


---

🏁 Script executed:

```shell
#!/bin/bash
# Find HandlesCleanup.php and examine its structure
find . -name "HandlesCleanup.php" -type f

# Search for SignalableCommandInterface without exclusions
rg -n --type=php 'SignalableCommandInterface' 

# Search for handleSignal more broadly
rg -n --type=php 'handleSignal'

# Search for interface definitions in console directory
fd -e php . src/Console | head -20

Length of output: 695


Now let me examine the actual method implementation:


---

🏁 Script executed:

```shell
#!/bin/bash
# View the handleSignal method implementation in HandlesCleanup.php
cat src/Console/Traits/HandlesCleanup.php | sed -n '40,70p'

# Also check if $previousExitCode is used anywhere in the file
rg -n 'previousExitCode' src/Console/Traits/HandlesCleanup.php

Length of output: 1031


Let me verify the Symfony interface requirement:


---

🌐 Web query:

```web
Symfony SignalableCommandInterface handleSignal method signature

💡 Result:

Signature (Symfony 6.3+):
public function handleSignal(int $signal): int|false

(Older Symfony versions used : void; returning no value is deprecated since 6.3.) [1][2]


Remove the unused $previousExitCode parameter to comply with SignalableCommandInterface.

The Symfony interface signature is public function handleSignal(int $signal): int|false, but the implementation includes an extra parameter that is not used and violates the interface contract. Remove int|false $previousExitCode = 0 from line 46.

🧰 Tools
🪛 PHPMD (2.15.0)

46-46: Avoid unused parameters such as '$previousExitCode'. (undefined)

(UnusedFormalParameter)

🤖 Prompt for AI Agents
In src/Console/Traits/HandlesCleanup.php around line 46, the method signature
for handleSignal includes an unused extra parameter `int|false $previousExitCode
= 0` which violates Symfony's SignalableCommandInterface; remove the
`$previousExitCode` parameter so the signature becomes `public function
handleSignal(int $signal): int|false`, and update any internal references (none
expected) and method calls to match the interface.

Comment on lines +76 to +78
$this->app->singleton('db.transactions', function ($app) {
return new DatabaseTransactionsManager;
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Inject the database manager into DatabaseTransactionsManager.

DatabaseTransactionsManager expects the DatabaseManager in its constructor. Instantiating it with no arguments will immediately throw a TypeError when the binding is resolved.

-        $this->app->singleton('db.transactions', function ($app) {
-            return new DatabaseTransactionsManager;
-        });
+        $this->app->singleton('db.transactions', function ($app) {
+            return new DatabaseTransactionsManager($app['db']);
+        });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$this->app->singleton('db.transactions', function ($app) {
return new DatabaseTransactionsManager;
});
$this->app->singleton('db.transactions', function ($app) {
return new DatabaseTransactionsManager($app['db']);
});
🤖 Prompt for AI Agents
In src/Database/DatabaseServiceProvider.php around lines 76 to 78, the singleton
binding returns new DatabaseTransactionsManager with no arguments but the class
requires a DatabaseManager in its constructor; change the factory to resolve and
pass the database manager from the container (e.g.
$app->make(\Illuminate\Database\DatabaseManager::class) or $app['db']) into the
DatabaseTransactionsManager constructor so the dependency is injected when the
singleton is created.

Comment on lines +27 to +55
$changes = [$this->compileDropDefaultConstraint($blueprint, $command)];
$schema = $this->connection->getSchemaBuilder();
$table = $blueprint->getTable();

$oldColumns = collect($schema->getColumns($table));

foreach ($blueprint->getChangedColumns() as $column) {
$sql = sprintf(
'alter table %s alter column %s %s',
$this->wrapTable($blueprint),
$this->wrap($column),
$this->getType($column)
);

$oldColumn = $oldColumns->where('name', $column->name)->first();
if (!$oldColumn instanceof ColumnDefinition) {
$oldColumn = new ColumnDefinition($oldColumn);
}

foreach ($this->modifiers as $modifier) {
if (method_exists($this, $method = "modify{$modifier}")) {
$mod = strtolower($modifier);
$col = isset($oldColumn->{$mod}) && !isset($column->{$mod}) ? $oldColumn : $column;
$sql .= $this->{$method}($blueprint, $col);
}
}

$changes[] = $sql;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Initialize $changes without creating nested arrays.

compileDropDefaultConstraint may return an array of drop statements (Laravel 12 does this). Wrapping that array in another array leaves $changes shaped like [ [ ... ], 'alter ...' ], so the schema builder later tries to execute an array as SQL. Cast to array instead of wrapping it to keep the list flat.

-        $changes = [$this->compileDropDefaultConstraint($blueprint, $command)];
+        $changes = (array) $this->compileDropDefaultConstraint($blueprint, $command);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$changes = [$this->compileDropDefaultConstraint($blueprint, $command)];
$schema = $this->connection->getSchemaBuilder();
$table = $blueprint->getTable();
$oldColumns = collect($schema->getColumns($table));
foreach ($blueprint->getChangedColumns() as $column) {
$sql = sprintf(
'alter table %s alter column %s %s',
$this->wrapTable($blueprint),
$this->wrap($column),
$this->getType($column)
);
$oldColumn = $oldColumns->where('name', $column->name)->first();
if (!$oldColumn instanceof ColumnDefinition) {
$oldColumn = new ColumnDefinition($oldColumn);
}
foreach ($this->modifiers as $modifier) {
if (method_exists($this, $method = "modify{$modifier}")) {
$mod = strtolower($modifier);
$col = isset($oldColumn->{$mod}) && !isset($column->{$mod}) ? $oldColumn : $column;
$sql .= $this->{$method}($blueprint, $col);
}
}
$changes[] = $sql;
}
$changes = (array) $this->compileDropDefaultConstraint($blueprint, $command);
$schema = $this->connection->getSchemaBuilder();
$table = $blueprint->getTable();
$oldColumns = collect($schema->getColumns($table));
foreach ($blueprint->getChangedColumns() as $column) {
$sql = sprintf(
'alter table %s alter column %s %s',
$this->wrapTable($blueprint),
$this->wrap($column),
$this->getType($column)
);
$oldColumn = $oldColumns->where('name', $column->name)->first();
if (!$oldColumn instanceof ColumnDefinition) {
$oldColumn = new ColumnDefinition($oldColumn);
}
foreach ($this->modifiers as $modifier) {
if (method_exists($this, $method = "modify{$modifier}")) {
$mod = strtolower($modifier);
$col = isset($oldColumn->{$mod}) && !isset($column->{$mod}) ? $oldColumn : $column;
$sql .= $this->{$method}($blueprint, $col);
}
}
$changes[] = $sql;
}
🤖 Prompt for AI Agents
In src/Database/Schema/Grammars/SqlServerGrammar.php around lines 27 to 55,
$changes is being initialized as an array that contains the result of
compileDropDefaultConstraint, but that method can already return an array, which
produces a nested array and later causes the schema builder to attempt executing
an array as SQL; change the initialization to flatten the result by casting to
array or merging (e.g. $changes = (array)
$this->compileDropDefaultConstraint($blueprint, $command);) so $changes is a
flat list of SQL strings and subsequent code can append alter statements
normally.

Comment on lines +561 to 562
// @phpstan-ignore-next-line
$reflector = new ReflectionClass($this);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Consider investigating the root cause rather than suppressing.

The PHPStan suppression is part of a broader baseline expansion (136→513 issues). The PR discussion suggests triaging and fixing these issues rather than blanket-baselining them.

Run the following script to check what PHPStan error is being suppressed here:


🏁 Script executed:

#!/bin/bash
# Remove the phpstan-ignore and run analysis to see the actual error
vendor/bin/phpstan analyze src/Extension/ExtendableTrait.php --level=5 --no-progress 2>&1 | grep -A3 "line 562"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if phpstan exists and what version
if command -v vendor/bin/phpstan &> /dev/null; then
  echo "=== PHPStan Version ==="
  vendor/bin/phpstan --version
else
  echo "PHPStan not found in vendor/bin"
fi

# Try running phpstan on the file without grep filter
echo ""
echo "=== PHPStan analysis output ==="
vendor/bin/phpstan analyze src/Extension/ExtendableTrait.php --level=5 --no-progress 2>&1 | head -50

Length of output: 130


🏁 Script executed:

#!/bin/bash
# Check the actual file content around the line in question
echo "=== File content around lines 560-565 ==="
sed -n '560,565p' src/Extension/ExtendableTrait.php | cat -n

Length of output: 253


🏁 Script executed:

#!/bin/bash
# Look for any phpstan configuration files
echo "=== PHPStan config files ==="
find . -maxdepth 2 -name "phpstan.neon*" -o -name ".phpstan.*" | head -20

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Get broader context around the suppression - full method
echo "=== Context: Full method containing line 562 ==="
sed -n '550,575p' src/Extension/ExtendableTrait.php | cat -n

Length of output: 1110


🏁 Script executed:

#!/bin/bash
# Check class/trait definition at top of file
echo "=== File header and class/trait definition ==="
head -50 src/Extension/ExtendableTrait.php | cat -n

Length of output: 1768


🏁 Script executed:

#!/bin/bash
# Check phpstan.neon configuration
echo "=== PHPStan configuration ==="
cat ./phpstan.neon

Length of output: 567


🏁 Script executed:

#!/bin/bash
# Search for other phpstan-ignore suppressions in the same file
echo "=== Count of phpstan suppressions in this file ==="
grep -n "phpstan-ignore" src/Extension/ExtendableTrait.php | wc -l
echo ""
echo "=== All phpstan-ignore suppressions in this file ==="
grep -n "phpstan-ignore" src/Extension/ExtendableTrait.php

Length of output: 151


Fix the parameter type annotation instead of suppressing the error.

The suppression masks a legitimate type safety issue. The parameter $instance is typed as ?object (generic object), but line 15 calls $reflector->getParentClass(), which only exists on ReflectionClass. The fix is to change the parameter type from ?object $instance to ?ReflectionClass $instance so PHPStan can verify type safety, then remove the suppression at line 561.

🤖 Prompt for AI Agents
In src/Extension/ExtendableTrait.php around lines 561-562, remove the "//
@phpstan-ignore-next-line" and change the parameter type for $instance from
?object to ?ReflectionClass in the method signature that uses
$reflector->getParentClass(); update imports to include ReflectionClass (or use
\ReflectionClass) and adjust any callers if they currently pass a generic object
so they provide a ReflectionClass or null; this fixes the type-safety issue and
lets PHPStan validate the code.

Comment on lines +71 to 77
* @param string $path
* @return string
*/
public function publicPath()
public function publicPath($path = '')
{
return $this->basePath;
return $this->joinPaths($this->basePath, $path);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify that publicPath should return basePath instead of a public directory path.

The method name and docblock indicate this should return "the path to the public / web directory," but the implementation returns $this->basePath (when $path is empty). In standard Laravel, publicPath() returns the path to the public directory (e.g., /var/www/html/public), not the application base path (e.g., /var/www/html).

If Winter CMS intentionally places public files at the base path (no separate public directory), this is correct but should be documented. Otherwise, this appears to be a bug that could break file path resolution.

Additionally, this method uses joinPaths while other path methods in this class use PathResolver::join (see line 86 in langPath). Consider using PathResolver::join for consistency.

Run the following script to check how other Winter CMS/Storm path methods are implemented and whether a public directory exists:


🏁 Script executed:

#!/bin/bash
# Check if there's a publicPath property or constant that should be used
rg -n "publicPath\s*=" --type=php

# Check how publicPath was used in the old Laravel/Illuminate base class
rg -n "function publicPath" --type=php -A 5

# Look for references to 'public' directory in the codebase
rg -n -C3 "['\"]/public['\"]" --type=php | head -50

# Check other path resolution patterns in Application.php
rg -n "PathResolver::join.*basePath" --type=php

Length of output: 1894



---

🏁 Script executed:

```shell
#!/bin/bash
# Search for tests or usages of publicPath() method
rg -n "publicPath\(\)" --type=php -B2 -A2

# Check if there are any tests for the publicPath method
rg -n "publicPath" tests/ --type=php

# Look for any constants or properties defining public directory structure
rg -n "'public'|\"public\"" src/Foundation/Application.php -C2

# Check the joinPaths vs PathResolver::join usage in the file
rg -n "joinPaths|PathResolver::join" src/Foundation/Application.php

Length of output: 188


🏁 Script executed:

#!/bin/bash
# Search for usages and tests of publicPath() method
echo "=== publicPath() usages in codebase ==="
rg -n "publicPath\(\)" --type=php -B2 -A2

echo "=== publicPath tests ==="
rg -n "publicPath" tests/ --type=php -C2

echo "=== joinPaths method definition ==="
rg -n "function joinPaths|def joinPaths" --type=php -A5

echo "=== PathResolver::join vs joinPaths usage in Application.php ==="
rg -n "joinPaths|PathResolver::join" src/Foundation/Application.php -C1

Length of output: 1608


🏁 Script executed:

#!/bin/bash
# Find joinPaths method definition - check if it's in Application.php or parent class
echo "=== joinPaths definition in Application.php ==="
rg -n "function joinPaths|public function joinPaths" src/Foundation/Application.php -A3

# Check parent class or trait for joinPaths
echo "=== joinPaths in parent Application class or traits ==="
rg -n "joinPaths" src/Foundation/Application.php -B5 | head -20

# Look at class definition and extends
echo "=== Application class definition and parent ==="
head -30 src/Foundation/Application.php | grep -A5 "class Application"

# Look for any public directory constant or property
echo "=== public directory references ==="
rg -n "public.*=.*['\"]public" src/Foundation/Application.php

Length of output: 480


🏁 Script executed:

#!/bin/bash
# Find ApplicationBase import
echo "=== ApplicationBase import ==="
head -50 src/Foundation/Application.php | grep -E "use.*ApplicationBase|use.*Illuminate"

# Check if there's any documentation or comments about publicPath design
echo "=== Comments/docs around publicPath ==="
sed -n '65,80p' src/Foundation/Application.php

# Check git history for publicPath changes
echo "=== Recent publicPath changes ==="
git log -p --all -S "publicPath" -- src/Foundation/Application.php | head -100

# Look for any README or documentation about directory structure
echo "=== Directory structure docs ==="
find . -maxdepth 2 -name "*.md" -type f | xargs grep -l "public\|directory\|structure" 2>/dev/null | head -5

Length of output: 671



🏁 Script executed:

#!/bin/bash
# Check if there's a public directory in the repo
echo "=== Looking for public directories ==="
find . -maxdepth 3 -type d -name "public" 2>/dev/null

# Check if there's a storage/public or any public-like structure
echo "=== Directory structure at root ==="
ls -la | head -20

# Check what Laravel's publicPath typically expects
echo "=== Testing if joinPaths works like PathResolver::join ==="
rg -n "class.*joinPaths|function joinPaths" --type=php -A5 vendor/ 2>/dev/null | head -30

# Look for configuration files that might define paths
echo "=== Config files mentioning paths ==="
find . -maxdepth 3 -name "config.php" -o -name "*config*.php" 2>/dev/null | head -5

Length of output: 1290


🏁 Script executed:

#!/bin/bash
# Check if there's a public directory in the repo
echo "=== Looking for public directories ==="
find . -maxdepth 3 -type d -name "public" 2>/dev/null

# Check directory structure at root
echo "=== Root directory structure ==="
ls -la . | grep -E "^d" | head -20

# Check if joinPaths is standard Laravel utility
echo "=== Checking if joinPaths is from Laravel base class ==="
rg -n "joinPaths" vendor/laravel/framework/src/Illuminate/Foundation/Application.php -A2 2>/dev/null | head -20

# Look for how publicPath is typically used in Laravel/Illuminate
echo "=== Searching for public_path usage in codebase ==="
rg -n "public_path\(\)" --type=php -C2 | head -30

Length of output: 865


🌐 Web query:

What does Laravel's publicPath method return and how is it typically used?

💡 Result:

Laravel's publicPath (and the public_path() helper) returns the fully-qualified filesystem path to your application's public directory (e.g. /var/www/html/project/public). You can also pass a filename to get a path inside that directory: public_path('css/app.css') → /…/project/public/css/app.css. (laravel.com)

Typical uses

  • Build filesystem paths to public assets or files you need to read/write (images, generated files). (laravel.com)
  • Create responses that serve files from the public directory (or pass paths to file helpers).
  • Combine with URLs or asset helpers when you need the server path (not the web URL). (laravel.com)

Example

  • public_path() // returns public dir
  • public_path('uploads/image.jpg') // returns full path to that file. (laravel.com)

Citations:


🏁 Script executed:

#!/bin/bash
# Final check: Does Application.php define a $publicPath property?
echo "=== Properties in Application.php ==="
rg -n "protected.*publicPath|private.*publicPath|public.*publicPath" src/Foundation/Application.php

# Check pattern of other path properties
echo "=== Path properties like pluginsPath ==="
rg -n "protected.*Path|private.*Path" src/Foundation/Application.php | head -20

Length of output: 385


Fix publicPath() implementation to append the '/public' subdirectory and use PathResolver::join for consistency.

The current implementation returns $this->basePath instead of the public directory path, which breaks the Laravel publicPath() contract. Laravel's publicPath method should return the fully-qualified filesystem path to the public directory (e.g., /var/www/html/project/public), not the base path.

Additionally, this method is inconsistent with all other path methods in this class:

  • All other path methods (pluginsPath, themesPath, tempPath, uploadsPath, mediaPath) use PathResolver::join($this->basePath, '/subdirectory')
  • All other path methods have corresponding protected properties ($pluginsPath, $themesPath, etc.)
  • publicPath() has neither a property nor uses PathResolver::join

Suggested fix: Either add a $publicPath property and follow the pattern of other methods, or at minimum change line 76 to:

return PathResolver::join($this->basePath, '/public', $path);
🤖 Prompt for AI Agents
In src/Foundation/Application.php around lines 71 to 77, publicPath()
incorrectly returns the base path and doesn't follow the class pattern; add a
protected $publicPath property (like other path properties) and implement
publicPath($path = '') to return PathResolver::join($this->basePath, '/public',
$path) so it yields the fully-qualified public directory path and matches the
other path methods' use of PathResolver::join.

Comment on lines +35 to 42
* @param \Winter\Storm\Mail\MailManager $manager
* @return \Winter\Storm\Support\Testing\Fakes\MailFake
*/
public static function fake()
public static function fake(MailManager $manager)
{
static::swap($fake = new MailFake);
static::swap($fake = new MailFake($manager));

return $fake;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep Mail::fake() backward compatible.

Requiring callers to pass a MailManager breaks the longstanding Mail::fake() API used across Winter and downstream apps. Have fake() resolve the manager from the container when none is supplied so existing tests keep working.

-    public static function fake(MailManager $manager)
+    public static function fake(?MailManager $manager = null)
     {
-        static::swap($fake = new MailFake($manager));
+        $manager ??= static::getFacadeApplication()->make(MailManager::class);
+
+        static::swap($fake = new MailFake($manager));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* @param \Winter\Storm\Mail\MailManager $manager
* @return \Winter\Storm\Support\Testing\Fakes\MailFake
*/
public static function fake()
public static function fake(MailManager $manager)
{
static::swap($fake = new MailFake);
static::swap($fake = new MailFake($manager));
return $fake;
* @param \Winter\Storm\Mail\MailManager $manager
* @return \Winter\Storm\Support\Testing\Fakes\MailFake
*/
public static function fake(?MailManager $manager = null)
{
$manager ??= static::getFacadeApplication()->make(MailManager::class);
static::swap($fake = new MailFake($manager));
return $fake;
🤖 Prompt for AI Agents
In src/Support/Facades/Mail.php around lines 35 to 42, make fake() backward
compatible by allowing the MailManager parameter to be optional: change the
signature to accept MailManager|null (or no type hint) with a default null, and
when null resolve the manager from the container (e.g.,
Container::getInstance()->make(MailManager::class) or app(MailManager::class)).
Keep the existing static::swap($fake = new MailFake($manager)) and return $fake,
and update the docblock to indicate the manager is optional/nullable so existing
callers can call Mail::fake() without passing a manager.

Comment on lines +3 to +13
class EventTest extends \Winter\Storm\Tests\TestCase
{
public function __construct()
{
parent::__construct("testDummy");
}

public function testDummy()
{
$this->assertTrue(true, true);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Event fixture incorrectly extends TestCase.

This file is an event fixture used in tests/Events/DispatcherTest.php (line 46: $dispatcher->dispatch(new EventTest)), but it has been changed to extend \Winter\Storm\Tests\TestCase. Event fixtures should be simple POPOs (Plain Old PHP Objects), not test cases.

Additionally, the code has multiple issues:

  • Line 7: Hardcoding "testDummy" in the parent constructor is non-standard and serves no purpose for an event fixture
  • Line 12: $this->assertTrue(true, true) has incorrect signature - the second parameter should be a message string, not a boolean

Revert this file to a simple event class:

 <?php
 
-class EventTest extends \Winter\Storm\Tests\TestCase
+class EventTest
 {
-    public function __construct()
-    {
-        parent::__construct("testDummy");
-    }
-
-    public function testDummy()
-    {
-        $this->assertTrue(true, true);
-    }
 }
🤖 Prompt for AI Agents
In tests/fixtures/events/EventTest.php around lines 3 to 13, the file was
incorrectly changed to extend \Winter\Storm\Tests\TestCase and include
test-specific code; revert it to a plain event POPO: remove the extends clause,
remove the constructor and any test methods/assertions, and restore it as a
simple class (optionally with any public properties or methods the
DispatcherTest expects) so it can be instantiated with new EventTest() during
dispatching.

Comment on lines 23 to 28
public function setUp(): void
{
$traitName = 'Winter\Storm\Support\Traits\Emitter';
$this->traitObject = $this->getObjectForTrait($traitName);
$this->traitObject = new class {
use \Winter\Storm\Support\Traits\Emitter;
};
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add parent::setUp() call to ensure proper test initialization.

The anonymous class approach correctly replaces the deprecated PHPUnit getObjectForTrait() method. However, the parent::setUp() call is missing, which could prevent proper test environment initialization when extending TestbenchTestCase.

Apply this diff to add the parent setUp call:

 public function setUp(): void
 {
+    parent::setUp();
+
     $this->traitObject = new class {
         use \Winter\Storm\Support\Traits\Emitter;
     };
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function setUp(): void
{
$traitName = 'Winter\Storm\Support\Traits\Emitter';
$this->traitObject = $this->getObjectForTrait($traitName);
$this->traitObject = new class {
use \Winter\Storm\Support\Traits\Emitter;
};
}
public function setUp(): void
{
parent::setUp();
$this->traitObject = new class {
use \Winter\Storm\Support\Traits\Emitter;
};
}
🤖 Prompt for AI Agents
In tests/Support/EmitterTest.php around lines 23 to 28, the setUp() override is
missing a call to parent::setUp(), which can prevent proper testbench
initialization; modify the setUp() method to call parent::setUp() at the start
of the method, then proceed to instantiate the anonymous class that uses
\Winter\Storm\Support\Traits\Emitter so the test environment is initialized
before creating traitObject.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement PRs that implement a new feature or substantial change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants