Skip to main content

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 urls table
  • 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';
}