By default, when getting a model from a controller using Route Model Binding, Laravel will try to find a model using the parameter in your route, and associate it to the default identifier of the related table (most of the time, this is the "id" key).
// routes/web.php
use App\Models\Cart;
use Illuminate\Support\Facades\Route;
// --> What you see
Route::get("/cart/{cart}", function(Cart $cart) {
// $cart ready to be used
});
// --> What happens behind the scene
Route::get("/cart/{cart}", function(string $identifier) {
$cart = Cart::findOrFail($identifier);
// $cart ready to be used
});
This means if you offer the possibility to view your cart, you will expose the route /cart/12 for example. This is not ideal in terms of security because you now expose your cart database identifier, and if you forgot or made a mistake into your cart's policy, a malicious user can access the cart of other users (/cart/41).
In this context UUID are very useful because:
- They offer a good way to create random, hard to predict identifiers
- Can be manually generated from the code
- Are not likely to collide
The best scenarios would be to expose this uuid instead of your database auto incremented identifier, like /cart/e22b86bcb8e24cfea13856a0766bfef2.
The goal of this package is to simplify at best this task for you.
- Provide a trait to configure your Route Model Binding to use a slug column
- Provide an helper to create the slug column on your migration, according to your configuration
- Provide a scope to find your model by the slug column
- Allow you to customize the name of the slug column
- This package relies on these methods on your model, and if you override them the logic might not be guarranteed to keep working:
composer require khalyomede/laravel-eloquent-uuid-slug
On the model of your choice, use the Sluggable
trait.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Khalyomede\EloquentUuidSlug\Sluggable;
class Cart extends Model
{
use Sluggable;
}
The Sluggable
trait offers the method Sluggable::addSlugColumn()
to make this step a breeze for you.
use App\Models\Cart;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
final class CreateCartsTable extends Migration
{
public function up(): void
{
Schema::create('carts', function (Blueprint $table): void {
$table->id();
$table->string('name');
Cart::addSlugColumn($table);
$table->timestamps();
});
}
public function down(): void
{
Schema::drop('carts');
}
}
If you do not want the Sluggable::addSlugColumn(Blueprint)
to add SQL constraints (unique index and non nullable column), use its counterpart method Sluggable::addUnconstrainedSlugColumn(Blueprint)
.
use App\Models\Cart;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
final class CreateCartsTable extends Migration
{
public function up(): void
{
Schema::create('carts', function (Blueprint $table): void {
$table->id();
$table->string('name');
Cart::addUnconstrainedSlugColumn($table);
$table->timestamps();
});
}
public function down(): void
{
Schema::drop('carts');
}
}
- 1. Configure the slug column name
- 2. Use dashes for the generated UUID
- 3. Custom route model binding for specific routes
- 4. Customize the slug column in your migration
- 5. Retreive a model by its slug
- 6. Dropping the slug column
- 7. Validate a value exists by slug
By default the Sluggable
trait will assume the name of the slug column is slug
. Here is how to provide one that you prefer.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Khalyomede\EloquentUuidSlug\Sluggable;
class Cart extends Model
{
use Sluggable;
public function slugColumn(): string
{
return 'code';
}
}
By default, the Sluggable
trait will configure the UUID generator to remove dashes, to help make shorter URLs. If you prefer to keep them, here is how you can do it.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Khalyomede\EloquentUuidSlug\Sluggable;
class Cart extends Model
{
use Sluggable;
public function slugWithDashes(): bool
{
return true;
}
}
By default, all your models that use the Sluggable
trait will retreive their model using the slug column when performing Route Model Binding.
If you would like to bypass it for specific routes, you can customize the column used to retreive your model occasionally.
For example, this is how to retreive your Cart model using its id for a specific route.
// routes/web.php
use App\Models\Cart;
use Illuminate\Support\Facades\Route;
// --> What you see
Route::get("/cart/{cart:id}", function(Cart $cart) {
// $cart ready to be used
});
As a final resort, if this method does not work, you can always fallback to get the raw data from your route, and perform fetching your model yourself:
// routes/web.php
use App\Models\Cart;
use Illuminate\Support\Facades\Route;
// --> What you see
Route::get("/cart/{cart}", function(string $identifier) {
$cart = Cart::findOrFail($identifier);
// $cart ready to be used
});
You can use all the available column modifiers right after calling the method Sluggable::addSlugColumn()
, to re-order the column or add some comments for example.
use App\Models\Cart;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
final class CreateCartsTable extends Migration
{
public function up(): void
{
Schema::create('carts', function (Blueprint $table): void {
$table->id();
$table->string('name');
Cart::addSlugColumn($table)
->after('name')
->comment('Auto-generated by a package.');
$table->timestamps();
});
}
public function down(): void
{
Schema::drop('carts');
}
}
To help you manually fetching a model by its slug, you can use the Sluggable::scopeWithSlug()
scope to do it. It follows your configuration, so no matter how you named your slug column it will still work.
// routes/web.php
use App\Models\Cart;
use Illuminate\Support\Facades\Route;
Route::get("/cart/{cart}", function(string $identifier) {
$cart = Cart::withSlug($identifier)->firstOrFail();
// $cart ready to be used
});
The Sluggable::findBySlug()
, Sluggable::findBySlugOrFail()
, Sluggable::firstBySlug()
or Sluggable::firstBySlugOrFail()
methods also exist as a shorthand:
// routes/web.php
use App\Models\Cart;
use Illuminate\Support\Facades\Route;
Route::get("/cart/{cart}", function(string $identifier) {
$cart = Cart::findBySlugOrFail($identifier);
$cart = Cart::findBySlug($identifier);
$cart = Cart::where("id", ">=", 1)->firstBySlug($identifier);
$cart = Cart::where("id", ">=", 1)->firstBySlugOrFail($identifier);
// $cart ready to be used
});
You can use Sluggable::dropSlugColumn(Blueprint)
when you want to drop only the slug column on an existing table. Please follow the complete instructions on the "down" method since there is some gotchas to deal with.
use App\Models\Cart;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
final class DropSlugColumnOnCartsTable extends Migration
{
public function up(): void
{
Schema::create('carts', function (Blueprint $table): void {
Cart::dropSlugColumn($table);
});
}
public function down(): void
{
Schema::table('posts', function (Blueprint $table): void {
Cart::addUnconstrainedSlugColumn($table);
});
Schema::table('posts', function (Blueprint $table): void {
Cart::fillEmptySlugs();
Cart::constrainSlugColumn($table);
});
}
}
You can validate a model exists by the slug column you defined. This is equivalent to calling the existing "exists" rule:
"post_id" => "exists:posts,slug"
But without having to manually specify the slug column (it is fetched automatically according to wether you customized the name or not).
// app/Http/Controllers/PostController.php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Support\Facades\Validator;
use Khalyomede\EloquentUuidSlug\Rules\ExistsBySlug;
class PostController extends Controller
{
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
"post_id" => ["required", new ExistsBySlug(Post::class)],
]);
// ...
}
}
The table below shows the compatibility across Laravel, PHP and this package current version. For the compatibility regarding this package previous version, please browse another tag.
Laravel version | PHP version | Compatible |
---|---|---|
10.* | 8.3.* | ✅ |
10.* | 8.2.* | ✅ |
10.* | 8.1.* | ❌ |
9.* | 8.2.* | ❌ |
9.* | 8.1.* | ❌ |
9.* | 8.0.* | ❌ |
8.* | 8.2.* | ❌ |
8.* | 8.1.* | ❌ |
8.* | 8.0.* | ❌ |
8.* | 7.4.* | ❌ |
8.* | 7.3.* | ❌ |
7.x | * | ❌ |
To counter-check these results, you can use the Docker containers (see docker-compose.yml file) to run the tests described in the Tests section.
I created this package mostly to practice creating a tested laravel package, and toying with my first Github Workflow. There is some others high quality packages out there so make sure to take a look at them!
composer run test
composer run analyse
composer run check
composer run lint
composer run scan
composer run updates
Or
composer run all