UrlContract Interface
The UrlContract interface defines the contract that models must implement to use the HasUrl trait. It requires a single method that returns the canonical full URL for the model.
Namespace
JobMetric\Url\Contracts\UrlContract
Overview
UrlContract ensures that models using HasUrl can compute their full URLs. The returned URL is used for:
- Persistence: Stored in the
urlstable - Uniqueness checks: Global uniqueness enforcement
- Redirects: Legacy URL redirects to canonical URLs
- Routing resolution: Fallback route matching
Interface Definition
interface UrlContract
{
public function getFullUrl(): string;
}
Requirements
Deterministic
The getFullUrl() method must be deterministic and side-effect free. It should return the same value for the same model state.
// Good: Deterministic
public function getFullUrl(): string
{
return '/shop/' . ($this->category->slug ?? 'uncategorized') . '/' . ($this->slug ?? 'product-' . $this->id);
}
// Bad: Non-deterministic
public function getFullUrl(): string
{
return '/shop/' . $this->slug . '?t=' . time(); // Changes every call
}
Length Limit
The returned URL must be ≤ 2000 characters (database constraint).
Format Consistency
Choose ONE consistent format across your application:
- Path-only (recommended):
"category/product" - Absolute:
"https://example.com/category/product"
Recommended: Use path-only without leading slash for consistency.
Normalization
URLs should be normalized according to your URL policy:
- No trailing slashes (or consistent trailing slashes)
- Stable casing
- No query strings or fragments (canonical path only)
Implementation Examples
Example 1: Simple Product
class Product extends Model implements UrlContract
{
use HasUrl;
public function getFullUrl(): string
{
return '/shop/' . ($this->slug ?? 'product-' . $this->id);
}
}
Example 2: Product with Category
class Product extends Model implements UrlContract
{
use HasUrl;
public function category()
{
return $this->belongsTo(Category::class);
}
public function getFullUrl(): string
{
$categorySlug = optional($this->category)->slug ?? 'uncategorized';
$productSlug = $this->slug ?? 'product-' . $this->id;
return "/shop/{$categorySlug}/{$productSlug}";
}
}
Example 3: Hierarchical Pages
class Page extends Model implements UrlContract
{
use HasUrl;
public function parent()
{
return $this->belongsTo(Page::class, 'parent_id');
}
public function getFullUrl(): string
{
$segments = [];
$page = $this;
while ($page) {
array_unshift($segments, $page->slug ?? 'page-' . $page->id);
$page = $page->parent;
}
return '/' . implode('/', $segments);
}
}
Example 4: Blog Post with Category
class Post extends Model implements UrlContract
{
use HasUrl;
public function category()
{
return $this->belongsTo(Category::class);
}
public function getFullUrl(): string
{
$categorySlug = $this->category->slug ?? 'uncategorized';
$postSlug = $this->slug ?? 'post-' . $this->id;
return "/blog/{$categorySlug}/{$postSlug}";
}
}
Best Practices
1. Handle Missing Slugs
Always provide a fallback when slug might be null:
// Good: Fallback provided
return '/shop/' . ($this->slug ?? 'product-' . $this->id);
// Bad: May return null
return '/shop/' . $this->slug;
2. Use Optional Relationships
Use optional() or null coalescing for relationships:
// Good: Safe relationship access
$categorySlug = optional($this->category)->slug ?? 'uncategorized';
// Bad: May throw exception
$categorySlug = $this->category->slug;
3. Keep It Simple
Avoid complex logic in getFullUrl():
// Good: Simple and clear
public function getFullUrl(): string
{
return '/shop/' . ($this->slug ?? 'product-' . $this->id);
}
// Bad: Too complex
public function getFullUrl(): string
{
$base = config('app.url');
$locale = app()->getLocale();
$category = $this->category ? $this->category->slug : 'uncategorized';
$product = $this->slug ?? 'product-' . $this->id;
return "{$base}/{$locale}/shop/{$category}/{$product}";
}
Common Mistakes
Mistake 1: Non-Deterministic URLs
// Bad: Changes every call
public function getFullUrl(): string
{
return '/shop/' . $this->slug . '?ref=' . time();
}
Mistake 2: Missing Fallbacks
// Bad: May return null
public function getFullUrl(): string
{
return '/shop/' . $this->slug;
}
Mistake 3: Including Query Strings
// Bad: Query strings should not be in canonical URL
public function getFullUrl(): string
{
return '/shop/' . $this->slug . '?utm_source=email';
}
Related Documentation
- HasUrl Trait - Core trait that uses this interface
- Url Model - Model that stores full URLs