Skip to content

Conversation

vlados
Copy link
Owner

@vlados vlados commented Oct 1, 2025

Summary

  • Add validation in HasUniqueUrls trait initialization
  • Check if model has 'url' or 'urls' database columns
  • Throw descriptive exception with guidance on fixing conflicts
  • Add tests to verify exception behavior

Fixes #67

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Adds automatic detection of conflicting URL-related columns and surfaces clear, user-friendly errors during initialization.
  • Bug Fixes
    • Prevents hidden conflicts with URL fields from causing unexpected behavior by validating schema access safely and failing fast with descriptive messages.
  • Tests
    • Adds tests ensuring exceptions are raised for conflicting column names and that error messages reference the offending column.

github-actions bot and others added 2 commits October 1, 2025 20:53
- Add validation in HasUniqueUrls trait initialization
- Check if model has 'url' or 'urls' database columns
- Throw descriptive exception with guidance on fixing conflicts
- Add tests (test #12 and #13) to verify exception behavior

Fixes #67

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Vladislav Stoitsov <vlados@users.noreply.github.com>
Copy link

coderabbitai bot commented Oct 1, 2025

Walkthrough

Adds an initializer to the HasUniqueUrls trait that checks for conflicting 'url' or 'urls' database columns during model initialization and throws an exception if found; adds tests that create tables with those columns and assert the exception is thrown.

Changes

Cohort / File(s) Summary
Trait initialization and attribute conflict checks
src/HasUniqueUrls.php
Added public function initializeHasUniqueUrls(): void. Implemented private checkForConflictingAttributes() to detect 'url' and 'urls' columns and throw an exception referencing the model class. Added private hasColumn(string $column) that queries the schema with exception handling to skip checks when schema access is unavailable.
Tests for conflicting columns
tests/HasUniqueUrlsTest.php
Added two tests that create tables containing a url or urls column, define inline models using the HasUniqueUrls trait, assert that creating a record throws the expected exception mentioning the conflicting column, and drop the tables. Added necessary imports (Blueprint, Schema) for schema manipulation.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Dev as Developer
  participant M as Model (uses HasUniqueUrls)
  participant T as HasUniqueUrls Trait
  participant DB as Schema/DB

  Dev->>M: Define model using HasUniqueUrls
  activate M
  M->>T: initializeHasUniqueUrls()
  activate T
  T->>T: checkForConflictingAttributes()
  T->>DB: hasColumn('url') / hasColumn('urls')
  alt Column exists
    T-->>M: Throw exception (conflicting column)
  else Schema unavailable or no column
    T-->>M: Continue initialization
  end
  deactivate T
  deactivate M
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title succinctly describes the primary new feature of raising an exception when models have conflicting "url" or "urls" columns, which aligns precisely with the changes made to the HasUniqueUrls trait initialization and the added tests.
Linked Issues Check ✅ Passed The PR implements detection of "url" and "urls" columns in the HasUniqueUrls::initializeHasUniqueUrls method by invoking checkForConflictingAttributes, which throws descriptive exceptions when either column exists as required by issue #67, and it adds tests that assert these exceptions for both "url" and "urls" columns.
Out of Scope Changes Check ✅ Passed All modifications are confined to detecting conflicts on "url" and "urls" attributes or columns in the HasUniqueUrls trait and adding corresponding tests, with no unrelated files or features introduced beyond the scope of issue #67.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch claude/issue-67-20251001-2050

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

Copy link
Contributor

claude bot commented Oct 1, 2025

Pull Request Review: feat: add exception for models with conflicting url/urls columns

Summary

This PR adds validation to detect conflicting url or urls database columns when using the HasUniqueUrls trait, addressing issue #67. Overall, this is a solid enhancement that improves developer experience by providing early error detection.


✅ Strengths

  1. Clear problem identification: The PR directly addresses a known conflict issue
  2. Good error messaging: The exception provides clear guidance on how to resolve the conflict
  3. Comprehensive test coverage: Both url and urls conflict scenarios are tested
  4. Non-breaking: Uses trait initialization pattern that Laravel supports natively

🔍 Code Quality Issues

1. Exception Type (src/HasUniqueUrls.php:44)

Issue: Using generic Exception class instead of a custom or more specific exception type.

Current:

throw new Exception(
    "Model [{$modelClass}] has a conflicting column '{$attribute}'. " ...
);

Recommendation: Create a custom exception or use \RuntimeException or \InvalidArgumentException for better exception handling and type specificity.

throw new \InvalidArgumentException(
    "Model [{$modelClass}] has a conflicting column '{$attribute}'. " ...
);

2. Incomplete Comment (src/HasUniqueUrls.php:42)

Issue: Comment mentions checking "fillable, guarded, or as a database column" but implementation only checks database columns.

Current:

// Check if attribute exists in fillable, guarded, or as a database column
if ($this->hasColumn($attribute)) {

Recommendation: Either update the comment to match implementation or extend the check:

// Check if attribute exists as a database column
if ($this->hasColumn($attribute)) {

3. Silent Exception Swallowing (src/HasUniqueUrls.php:64)

Issue: The catch block silently swallows all exceptions without logging or providing feedback.

Current:

} catch (\Exception $e) {
    // If we can't check the schema (e.g., during testing without migrations), skip the check
}

Concern: This could hide genuine errors (connection issues, permission problems, etc.) beyond just missing migrations.

Recommendation: Be more specific about when to skip:

} catch (\Exception $e) {
    // Skip check if table doesn't exist (e.g., during testing)
    // Log other errors for debugging
    if (!str_contains($e->getMessage(), 'does not exist') && 
        !str_contains($e->getMessage(), 'no such table')) {
        // Consider logging: \Log::warning("Unable to check schema: " . $e->getMessage());
    }
}

🐛 Potential Bugs

4. Timing Issue: Check happens after model instantiation

Critical Issue: The initializeHasUniqueUrls() method is called during model instantiation, but the column check requires a database connection. This means:

  • The check only runs when creating/instantiating models
  • If you retrieve an existing model from the database, the check runs unnecessarily
  • If the table doesn't exist yet (migrations not run), the check is silently skipped

Test verification concern: Looking at tests 12 and 13 (lines 175-247), they test ::create() which triggers the check. However, they don't test:

  • Direct instantiation: new $modelClass()
  • Retrieval: $modelClass::find(1)
  • Query builder results

Recommendation: Consider moving the check to a static boot method that runs once per model class:

protected static function bootHasUniqueUrls(): void
{
    static::creating(static function (Model $model): void {
        // Run check only on first instance
        if (!static::$conflictCheckRun ?? false) {
            $model->checkForConflictingAttributes();
            static::$conflictCheckRun = true;
        }
        
        if ($model->isAutoGenerateUrls()) {
            // existing created logic
        }
    });
    
    // ... rest of boot logic
}

⚡ Performance Considerations

5. Schema check on every model instantiation

Issue: hasColumn() performs a database query via getSchemaBuilder()->hasColumn() on every model instantiation.

Impact:

  • Unnecessary DB queries when loading multiple models
  • Performance degradation in loops or collection operations

Recommendation: Cache the schema check result:

  • Use a static property to cache per model class
  • Or leverage Laravel's schema cache (php artisan schema:cache)
private static array $checkedModels = [];

private function checkForConflictingAttributes(): void
{
    $modelClass = get_class($this);
    
    // Skip if already checked for this model class
    if (isset(self::$checkedModels[$modelClass])) {
        return;
    }
    
    // ... existing check logic ...
    
    self::$checkedModels[$modelClass] = true;
}

🔒 Security Considerations

✅ No security concerns identified. The code doesn't handle user input or sensitive data.


🧪 Test Coverage

Strengths:

  • Tests cover both url and urls column conflicts
  • Tests properly create/drop test tables
  • Exception messages are verified

Gaps:

  1. Missing test: What happens when table doesn't exist?
  2. Missing test: Does the check run on ::find() or ::first() (and should it)?
  3. Missing test: Performance - does check run multiple times unnecessarily?

Suggested additional tests:

test('14. Check validation is skipped when table does not exist', function () {
    $modelClass = new class () extends \Illuminate\Database\Eloquent\Model {
        use \Vlados\LaravelUniqueUrls\HasUniqueUrls;
        protected $table = 'non_existent_table';
        // ... rest of setup
    };
    
    // Should not throw exception
    expect(fn () => new $modelClass())->not->toThrow(Exception::class);
});

test('15. Check validation runs only once per model class', function () {
    Schema::create('test_table', function (Blueprint $table) {
        $table->increments('id');
        $table->string('name');
        $table->string('url');
    });
    
    // Count DB queries to verify check runs only once
    // ... implementation
    
    Schema::dropIfExists('test_table');
});

📋 Recommendations Summary

High Priority:

  1. ✅ Add caching to prevent repeated schema checks (Performance)
  2. ⚠️ Change from generic Exception to InvalidArgumentException
  3. ⚠️ Fix misleading comment about fillable/guarded checking

Medium Priority:
4. Consider more specific exception handling in hasColumn()
5. Add tests for edge cases (non-existent tables, multiple instantiations)
6. Consider checking in boot method instead of init method

Low Priority:
7. Document the performance implications of schema checking


🎯 Conclusion

This PR successfully addresses issue #67 and adds valuable validation. The implementation is functional but would benefit from performance optimization (caching) and better exception handling. The test coverage is good but could be enhanced with edge case testing.

Recommendation:Approve with minor changes - The current implementation is acceptable, but I strongly suggest implementing the caching mechanism before merging to avoid performance issues in production.

Great work on the clear error messaging and comprehensive testing! 🚀

@vlados
Copy link
Owner Author

vlados commented Oct 1, 2025

@claude some checks are failing

Copy link
Contributor

claude bot commented Oct 1, 2025

Claude finished @vlados's task —— View job


Fixed failing tests ✅

  • Check CI status and identify failing checks
  • Get detailed logs for failed workflows
  • Analyze the failures and identify root cause
  • Fix the issues
  • Commit and push fixes

Root Cause

Tests 12 and 13 were failing because the exception is being thrown during model instantiation (when the anonymous class is created), not during the ::create() call. The initializeHasUniqueUrls() method runs when the model is first instantiated, so the exception was happening before the expect() assertion could catch it.

Fix Applied

Wrapped the entire model class creation inside the expect closure so the exception can be properly caught and tested. The tests now correctly validate that the exception is thrown when models have conflicting url or urls columns.

Changes committed: 2420585

The CI checks should pass now! 🚀


Job RunBranch

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: 1

🧹 Nitpick comments (2)
src/HasUniqueUrls.php (1)

53-68: Consider catching more specific exceptions.

Catching the generic \Exception on line 63 may silently ignore unexpected errors beyond schema access issues. Laravel typically throws specific exceptions for schema-related problems.

Consider catching more specific exceptions:

     private function hasColumn(string $column): bool
     {
         try {
             // Check if the table exists and has the column
             if ($this->getConnection()->getSchemaBuilder()->hasColumn($this->getTable(), $column)) {
                 return true;
             }
-        } catch (\Exception $e) {
-            // If we can't check the schema (e.g., during testing without migrations), skip the check
+        } catch (\Illuminate\Database\QueryException | \PDOException $e) {
+            // If we can't check the schema (e.g., table doesn't exist), skip the check
+            // This allows models to be instantiated before migrations run
         }
 
         return false;
     }

This ensures only database/schema-related exceptions are caught while allowing other unexpected errors to surface.

tests/HasUniqueUrlsTest.php (1)

212-247: Apply the same cleanup improvements as test 12.

This test has an identical structure to test 12 and would benefit from the same cleanup improvements (dropping table before creation and using try-finally for guaranteed cleanup).

Additionally, consider extracting a helper function to reduce duplication between tests 12 and 13:

function createConflictingColumnTest(string $columnName): void
{
    $tableName = "models_with_{$columnName}_column";
    
    Schema::dropIfExists($tableName);
    Schema::create($tableName, function (Blueprint $table) use ($columnName) {
        $table->increments('id');
        $table->string('name');
        $table->string($columnName);
    });
    
    try {
        $modelClass = new class ($tableName) extends \Illuminate\Database\Eloquent\Model {
            use \Vlados\LaravelUniqueUrls\HasUniqueUrls;
            
            protected $guarded = [];
            public $timestamps = false;
            
            public function __construct(string $table = null, array $attributes = [])
            {
                $this->table = $table ?? $this->table;
                parent::__construct($attributes);
            }
            
            public function urlHandler(): array
            {
                return [
                    'controller' => \Vlados\LaravelUniqueUrls\Tests\TestUrlHandler::class,
                    'method' => 'view',
                    'arguments' => [],
                ];
            }
            
            public function urlStrategy($language, $locale): string
            {
                return \Illuminate\Support\Str::slug($this->name);
            }
        };
        
        expect(fn () => $modelClass::create(['name' => 'test', $columnName => "test-{$columnName}"]))
            ->toThrow(Exception::class, "has a conflicting column '{$columnName}'");
    } finally {
        Schema::dropIfExists($tableName);
    }
}

test('12. Check if exception is thrown when model has conflicting url column', function () {
    createConflictingColumnTest('url');
});

test('13. Check if exception is thrown when model has conflicting urls column', function () {
    createConflictingColumnTest('urls');
});

This eliminates duplication while maintaining test clarity.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1f53afa and f43a46a.

📒 Files selected for processing (2)
  • src/HasUniqueUrls.php (1 hunks)
  • tests/HasUniqueUrlsTest.php (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tests/HasUniqueUrlsTest.php (4)
src/HasUniqueUrls.php (2)
  • urlHandler (19-19)
  • urlStrategy (105-108)
tests/Models/TestModel.php (2)
  • urlHandler (36-43)
  • urlStrategy (26-29)
tests/Models/ChildModel.php (2)
  • urlHandler (35-42)
  • urlStrategy (30-33)
tests/TestUrlHandler.php (1)
  • TestUrlHandler (7-13)
🪛 GitHub Actions: run-tests
tests/HasUniqueUrlsTest.php

[error] 184-184: Test 12 failed: exception thrown when model has conflicting url column. HasUniqueUrls trait throws a conflicting column error.


[error] 221-221: Test 13 failed: exception thrown when model has conflicting urls column. HasUniqueUrls trait throws a conflicting column error.

src/HasUniqueUrls.php

[error] 44-48: Model [{$modelClass}] has a conflicting column '{$attribute}'. The HasUniqueUrls trait uses 'urls' as a relationship name and provides 'relative_url' and 'absolute_url' attributes. Please rename the '{$attribute}' column in your model to avoid conflicts.

🔇 Additional comments (3)
src/HasUniqueUrls.php (1)

21-29: LGTM! Proper use of Laravel's trait initialization pattern.

The initializeHasUniqueUrls() method follows Laravel's convention for trait initialization, ensuring conflict detection occurs automatically when a model instance is created.

tests/HasUniqueUrlsTest.php (2)

3-3: LGTM! Required imports for table schema manipulation.

The Blueprint and Schema imports are necessary for the new tests that create and drop tables with conflicting columns.

Also applies to: 5-5


175-210: Cannot run tests in this sandbox (missing artisan/phpunit). Please execute the updated test locally to confirm it now passes.

Comment on lines +36 to +51
private function checkForConflictingAttributes(): void
{
$conflictingAttributes = ['url', 'urls'];
$modelClass = get_class($this);

foreach ($conflictingAttributes as $attribute) {
// Check if attribute exists in fillable, guarded, or as a database column
if ($this->hasColumn($attribute)) {
throw new Exception(
"Model [{$modelClass}] has a conflicting column '{$attribute}'. " .
"The HasUniqueUrls trait uses 'urls' as a relationship name and provides 'relative_url' and 'absolute_url' attributes. " .
"Please rename the '{$attribute}' column in your model to avoid conflicts."
);
}
}
}
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

Update the misleading comment on line 42.

The comment states "Check if attribute exists in fillable, guarded, or as a database column" but the implementation only checks database columns via hasColumn(). This could confuse future maintainers.

Apply this diff to fix the comment:

-            // Check if attribute exists in fillable, guarded, or as a database column
+            // Check if attribute exists as a database column

Alternatively, if you intended to also check fillable and guarded arrays, consider adding those checks. For example:

if ($this->hasColumn($attribute) || 
    in_array($attribute, $this->getFillable()) || 
    (! $this->isGuarded($attribute) && count($this->getGuarded()) > 0)) {

However, checking only database columns is likely sufficient for this use case since that's where the actual conflict would occur.

🧰 Tools
🪛 GitHub Actions: run-tests

[error] 44-48: Model [{$modelClass}] has a conflicting column '{$attribute}'. The HasUniqueUrls trait uses 'urls' as a relationship name and provides 'relative_url' and 'absolute_url' attributes. Please rename the '{$attribute}' column in your model to avoid conflicts.

The exception is thrown during model initialization (when using the trait),
not during the create() call. This fix wraps the entire model class creation
inside the expect closure so the exception can be properly caught.

Co-authored-by: Vladislav Stoitsov <vlados@users.noreply.github.com>
Copy link
Contributor

claude bot commented Oct 1, 2025

PR Review: Add exception for models with conflicting url/urls columns

Summary

This PR adds validation to detect conflicting url or urls database columns when using the HasUniqueUrls trait. This is a valuable improvement that prevents runtime errors and provides clear guidance to developers.

✅ Strengths

  1. Clear Error Messages: The exception message is descriptive and provides actionable guidance on how to fix the issue.
  2. Good Test Coverage: Two new tests (12 & 13) cover both url and urls column conflicts.
  3. Follows Conventions: Uses the same Exception class as the rest of the codebase (line 7 in HasUniqueUrls.php).
  4. Graceful Degradation: The hasColumn() method catches exceptions during schema checks, preventing issues during testing.

🔍 Code Quality Issues

1. Silent Exception Swallowing (src/HasUniqueUrls.php:64)

} catch (\Exception $e) {
    // If we can't check the schema (e.g., during testing without migrations), skip the check
}

Issue: This silently catches ALL exceptions, which could hide legitimate errors (connection issues, permission problems, etc.).

Recommendation: Be more specific about what exceptions to catch:

} catch (\Doctrine\DBAL\Exception|\PDOException $e) {
    // Schema inspection failed - skip validation
    return false;
}

2. Inconsistent Return Type Declaration (src/HasUniqueUrls.php:56)

The hasColumn() method has a return type but checkForConflictingAttributes() doesn't, even though both are private helper methods.

Recommendation: Add return type for consistency:

private function checkForConflictingAttributes(): void

This is already correct in the code - good job!

3. Missing Type Import (src/HasUniqueUrls.php:7)

The code uses Exception but should use the fully qualified name or import it properly.

Current: use Exception; (line 7)
Concern: This is the base PHP Exception class. Consider if a more specific Laravel exception would be better (e.g., RuntimeException, LogicException, or a custom exception).

🐛 Potential Bugs

1. Timing Issue in Tests (tests/HasUniqueUrlsTest.php:208 & 247)

$modelClass::create(['name' => 'test', 'url' => 'test-url']);

Issue: The initializeHasUniqueUrls() method is called during model instantiation, but the test creates the model with ::create(). The validation happens when the anonymous class is instantiated, not when create() is called.

Concern: The test might be passing for the wrong reason. The exception is thrown during the first instantiation (line 186/225), not during the create call.

Test Fix: The test should be more explicit:

expect(function () {
    $model = new class () extends \Illuminate\Database\Eloquent\Model {
        use \Vlados\LaravelUniqueUrls\HasUniqueUrls;
        // ... rest of definition
    };
})->toThrow(Exception::class, "has a conflicting column 'url'");

2. Database State Pollution

The tests create and drop tables but run within the same test suite. If test 12 fails before dropIfExists, test 13 could be affected.

Recommendation: Use try-finally blocks or ensure cleanup in afterEach:

try {
    expect(function () {
        // test code
    })->toThrow(...);
} finally {
    Schema::dropIfExists('models_with_url_column');
}

⚡ Performance Considerations

Schema Inspection on Every Model Instantiation

The initializeHasUniqueUrls() runs on EVERY model instance creation, performing a database query to check the schema.

Impact:

  • High-traffic applications could see performance degradation
  • Each model instantiation triggers getSchemaBuilder()->hasColumn() which is a database query

Recommendations:

  1. Cache the result: Use a static property to cache the check result per model class
  2. Move to boot method: Consider checking during bootHasUniqueUrls() instead of initialization
  3. Development-only check: Consider making this a development-time check only

Example optimization:

private static array $validatedModels = [];

public function initializeHasUniqueUrls(): void
{
    $class = static::class;
    if (!isset(self::$validatedModels[$class])) {
        $this->checkForConflictingAttributes();
        self::$validatedModels[$class] = true;
    }
}

🔒 Security Concerns

No security issues identified. The code doesn't handle user input or expose sensitive data.

📋 Test Coverage

Coverage: Good ✅

  • ✅ Tests both url and urls column conflicts
  • ✅ Tests verify exception messages contain the column name
  • ✅ Proper cleanup with dropIfExists

Suggested Additional Tests:

  1. Test that models WITHOUT conflicts work normally - Regression test
  2. Test the caching behavior - If optimization is added
  3. Test behavior when schema check fails - Verify graceful degradation

📝 Recommendations Summary

High Priority:

  1. Add caching to prevent performance issues - This is critical for production
  2. ⚠️ Be more specific with exception catching - Don't hide real errors

Medium Priority:

  1. 🔧 Fix test assertions - Verify exception is thrown at the right time
  2. 🔧 Add try-finally to tests - Prevent database pollution

Low Priority:

  1. 📚 Consider custom exception class - Better than generic Exception
  2. 📚 Add regression test - Ensure valid models still work

Overall Assessment

Status: ✅ Approved with suggestions

This is a solid improvement that addresses a real pain point (issue #67). The code is well-structured and the error messages are helpful. The main concern is the performance impact of schema inspection on every model instantiation. I recommend implementing the caching solution before merging.

Code Quality: 7/10 (good structure, needs optimization)
Test Coverage: 8/10 (good coverage, minor test improvements needed)
Documentation: 9/10 (excellent error messages)

Great work! 🎉


🤖 Review generated with Claude Code

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: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f43a46a and 2420585.

📒 Files selected for processing (1)
  • tests/HasUniqueUrlsTest.php (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tests/HasUniqueUrlsTest.php (2)
src/HasUniqueUrls.php (2)
  • urlHandler (19-19)
  • urlStrategy (105-108)
tests/Models/TestModel.php (2)
  • urlHandler (36-43)
  • urlStrategy (26-29)
🔇 Additional comments (1)
tests/HasUniqueUrlsTest.php (1)

3-5: LGTM!

The new imports for Blueprint and Schema are correctly added to support table creation and manipulation in the new tests.

Comment on lines +175 to +212
test('12. Check if exception is thrown when model has conflicting url column', function () {
// Create a table with 'url' column
Schema::create('models_with_url_column', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('url');
});

// Create a model class that uses HasUniqueUrls trait
// The exception should be thrown during model instantiation
expect(function () {
$modelClass = new class () extends \Illuminate\Database\Eloquent\Model {
use \Vlados\LaravelUniqueUrls\HasUniqueUrls;

protected $table = 'models_with_url_column';
protected $guarded = [];
public $timestamps = false;

public function urlHandler(): array
{
return [
'controller' => \Vlados\LaravelUniqueUrls\Tests\TestUrlHandler::class,
'method' => 'view',
'arguments' => [],
];
}

public function urlStrategy($language, $locale): string
{
return \Illuminate\Support\Str::slug($this->name);
}
};

$modelClass::create(['name' => 'test', 'url' => 'test-url']);
})->toThrow(Exception::class, "has a conflicting column 'url'");

Schema::dropIfExists('models_with_url_column');
});
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 exception timing and improve test reliability.

Several concerns with this test:

  1. Exception timing mismatch: The comment on line 184 states the exception should be thrown during "model instantiation," but the test wraps create() (line 208) which both instantiates and persists the model. According to the PR objectives, the check happens in initializeHasUniqueUrls() during trait initialization (when the model is instantiated). The current test may pass, but it's not clear whether the exception occurs during instantiation or later during the create operation.

  2. Table cleanup risk: Line 211's dropIfExists executes outside the expect() closure. If the assertion fails or an unexpected exception is thrown, the table won't be cleaned up, potentially affecting subsequent tests.

  3. Generic exception type: Using Exception::class is broad. Consider verifying whether a more specific exception type is thrown (e.g., RuntimeException, LogicException, or a custom exception class).

Consider this refactor to make the test more explicit and reliable:

 test('12. Check if exception is thrown when model has conflicting url column', function () {
     // Create a table with 'url' column
     Schema::create('models_with_url_column', function (Blueprint $table) {
         $table->increments('id');
         $table->string('name');
         $table->string('url');
     });
 
-    // Create a model class that uses HasUniqueUrls trait
-    // The exception should be thrown during model instantiation
-    expect(function () {
+    try {
+        // The exception should be thrown during model instantiation
         $modelClass = new class () extends \Illuminate\Database\Eloquent\Model {
             use \Vlados\LaravelUniqueUrls\HasUniqueUrls;
 
             protected $table = 'models_with_url_column';
             protected $guarded = [];
             public $timestamps = false;
 
             public function urlHandler(): array
             {
                 return [
                     'controller' => \Vlados\LaravelUniqueUrls\Tests\TestUrlHandler::class,
                     'method' => 'view',
                     'arguments' => [],
                 ];
             }
 
             public function urlStrategy($language, $locale): string
             {
                 return \Illuminate\Support\Str::slug($this->name);
             }
         };
-
-        $modelClass::create(['name' => 'test', 'url' => 'test-url']);
-    })->toThrow(Exception::class, "has a conflicting column 'url'");
-
-    Schema::dropIfExists('models_with_url_column');
+        
+        // If we reach here, no exception was thrown during instantiation
+        // Try to trigger it via create() as fallback
+        $modelClass::create(['name' => 'test', 'url' => 'test-url']);
+        
+        throw new \Exception('Expected exception was not thrown');
+    } catch (\Exception $e) {
+        expect($e->getMessage())->toContain("has a conflicting column 'url'");
+    } finally {
+        Schema::dropIfExists('models_with_url_column');
+    }
 });

Alternatively, verify the exact behavior by running:

Also verify the exception type being thrown:


🏁 Script executed:

#!/bin/bash
# Check the HasUniqueUrls trait implementation to confirm when the exception is thrown
ast-grep --pattern $'trait HasUniqueUrls {
  $$$
  initializeHasUniqueUrls($$$) {
    $$$
  }
  $$$
}'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for the exception being thrown for conflicting columns
rg -nP --type=php -A3 -B3 "throw.*conflicting column"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Locate the HasUniqueUrls trait file
rg -lP '\btrait\s+HasUniqueUrls' -g '*.php'
# Inspect the first match if found
FILE=$(rg -lP '\btrait\s+HasUniqueUrls' -g '*.php' | head -n1)
echo "Trait file: $FILE"
if [ -n "$FILE" ]; then
  echo "=== File Header ==="
  sed -n '1,200p' "$FILE"
  echo "=== initializeHasUniqueUrls ==="
  rg -n "function initializeHasUniqueUrls" -C3 "$FILE"
  echo "=== throw statements ==="
  rg -n "throw new" -C3 "$FILE"
fi

Length of output: 5092


Clarify exception timing and enforce cleanup

  • Reflect that the conflicting‐column exception is thrown in initializeHasUniqueUrls() during model construction; you can trigger it by instantiating the anonymous model (new class …) without calling create().
  • Move Schema::dropIfExists('models_with_url_column') into a finally block (or use Pest’s afterEach) to guarantee teardown even if the assertion fails.
  • (Optional) Replace the generic \Exception with a dedicated exception class for conflicting columns.
🤖 Prompt for AI Agents
In tests/HasUniqueUrlsTest.php around lines 175 to 212, the test currently
expects the conflicting-column exception to occur on create(), but the exception
is actually thrown during model construction in initializeHasUniqueUrls();
update the test to instantiate the anonymous model (new class ...) to trigger
the exception immediately instead of calling create(), and wrap the
Schema::dropIfExists('models_with_url_column') call in a finally block (or use
Pest’s afterEach) so the table is always dropped regardless of test outcome;
optionally replace the generic \Exception assertion with the specific
conflicting-column exception class if one exists.

Comment on lines +214 to +251
test('13. Check if exception is thrown when model has conflicting urls column', function () {
// Create a table with 'urls' column
Schema::create('models_with_urls_column', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('urls');
});

// Create a model class that uses HasUniqueUrls trait
// The exception should be thrown during model instantiation
expect(function () {
$modelClass = new class () extends \Illuminate\Database\Eloquent\Model {
use \Vlados\LaravelUniqueUrls\HasUniqueUrls;

protected $table = 'models_with_urls_column';
protected $guarded = [];
public $timestamps = false;

public function urlHandler(): array
{
return [
'controller' => \Vlados\LaravelUniqueUrls\Tests\TestUrlHandler::class,
'method' => 'view',
'arguments' => [],
];
}

public function urlStrategy($language, $locale): string
{
return \Illuminate\Support\Str::slug($this->name);
}
};

$modelClass::create(['name' => 'test', 'urls' => 'test-urls']);
})->toThrow(Exception::class, "has a conflicting column 'urls'");

Schema::dropIfExists('models_with_urls_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 | 🟡 Minor

Same concerns as test 12, plus code duplication.

This test has the same structural issues as test 12:

  1. Exception timing mismatch: Comment says instantiation, but test wraps create().
  2. Table cleanup risk: dropIfExists outside the exception-handling closure.
  3. Generic exception type: Consider a more specific exception class.
  4. Code duplication: Tests 12 and 13 are nearly identical except for the column name ('url' vs 'urls') and table name.

Consider extracting a helper function or using Pest's dataset feature to reduce duplication:

test('12. Check if exception is thrown when model has conflicting url column', function () {
    testConflictingColumn('url');
});

test('13. Check if exception is thrown when model has conflicting urls column', function () {
    testConflictingColumn('urls');
});

function testConflictingColumn(string $columnName): void
{
    $tableName = "models_with_{$columnName}_column";
    
    Schema::create($tableName, function (Blueprint $table) use ($columnName) {
        $table->increments('id');
        $table->string('name');
        $table->string($columnName);
    });
    
    try {
        $modelClass = new class ($tableName) extends \Illuminate\Database\Eloquent\Model {
            use \Vlados\LaravelUniqueUrls\HasUniqueUrls;
            
            protected $guarded = [];
            public $timestamps = false;
            
            public function __construct(string $table = null, array $attributes = [])
            {
                if ($table) {
                    $this->table = $table;
                }
                parent::__construct($attributes);
            }
            
            public function urlHandler(): array
            {
                return [
                    'controller' => \Vlados\LaravelUniqueUrls\Tests\TestUrlHandler::class,
                    'method' => 'view',
                    'arguments' => [],
                ];
            }
            
            public function urlStrategy($language, $locale): string
            {
                return \Illuminate\Support\Str::slug($this->name);
            }
        };
        
        $modelClass::create(['name' => 'test', $columnName => "test-{$columnName}"]);
        
        throw new \Exception('Expected exception was not thrown');
    } catch (\Exception $e) {
        expect($e->getMessage())->toContain("has a conflicting column '{$columnName}'");
    } finally {
        Schema::dropIfExists($tableName);
    }
}

Or use Pest's dataset feature:

test('check if exception is thrown when model has conflicting column', function (string $column) {
    // test implementation using $column
})->with(['url', 'urls']);
🤖 Prompt for AI Agents
tests/HasUniqueUrlsTest.php around lines 214 to 251: the test duplicates test 12
and has three issues — it asserts an exception on create() while the comment
says instantiation, it leaves Schema::dropIfExists outside exception handling
risking leftover state, and it uses a generic Exception and duplicates code; fix
by asserting the exception at the actual failing point (either instantiate the
anonymous model to trigger the trait check or wrap the create() call inside the
expect(...)->toThrow), move table cleanup into a finally block (or use
try/catch/finally) so Schema::dropIfExists always runs, replace Exception::class
with the more specific exception class thrown by the trait, and remove
duplication by extracting a helper function or using Pest datasets to run the
same test for 'url' and 'urls'.

@vlados vlados merged commit a98ecff into main Oct 1, 2025
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Check if model has attribute url or urls
1 participant