Skip to content

Instantly share code, notes, and snippets.

@scrubmx
Last active September 15, 2021 18:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save scrubmx/9bbdcf31ea574169a964a6c98a842960 to your computer and use it in GitHub Desktop.
Save scrubmx/9bbdcf31ea574169a964a6c98a842960 to your computer and use it in GitHub Desktop.
<?php
namespace App\Models\Traits;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
trait Sluggable
{
/**
* Boot the sluggable trait for a model.
*
* @return void
*/
public static function bootSluggable() : void
{
static::creating(function (Model $model) {
$model->addSlug();
});
static::updating(function (Model $model) {
$model->addSlug();
});
}
/**
* Execute the query and get the first result or throw an exception.
*
* @param string $slug
* @param array $columns
* @return static|null
*/
public static function findBySlug(string $slug, $columns = ['*'])
{
$column = (new static)->saveSlugTo();
return static::where($column, $slug)->first($columns);
}
/**
* Return the column name to generate the slug from.
*
* @return string
*/
public function buildSlugFrom() : string
{
return $this->sluggable['build_from'] ?? 'title';
}
/**
* Return the column name to save the slug to.
*
* @return string
*/
public function saveSlugTo() : string
{
return $this->sluggable['save_to'] ?? 'slug';
}
/**
* Add the slug to the model.
*
* @return void
*/
public function addSlug() : void
{
$key = $this->saveSlugTo();
if ($this->isClean($key)) {
$slug = $this->makeSlugUnique($this->generateNonUniqueSlug());
$this->setAttribute($key, $slug);
}
}
/**
* Generate a non unique slug for this record.
*
* @return string
*/
protected function generateNonUniqueSlug() : string
{
return Str::slug($this->getAttribute($this->buildSlugFrom()));
}
/**
* Make the given slug unique.
*
* @param string $slug
* @param int $suffix
* @return string
*/
protected function makeSlugUnique(string $slug, int $suffix = 1) : string
{
$originalSlug = $this->generateNonUniqueSlug();
while ($this->otherRecordExistsWithSlug($slug) || empty($slug)) {
$slug = $originalSlug.'-'.$suffix++;
}
return $slug;
}
/**
* Determine if a record exists with the given slug.
*
* @param string $slug
* @return bool
*/
protected function otherRecordExistsWithSlug(string $slug) : bool
{
return static::where($this->saveSlugTo(), $slug)
->where($this->getKeyName(), '!=', $this->getKey() ?? '0')
->withoutGlobalScopes()
->exists();
}
}
<?php
namespace Tests\Unit\Models\Traits;
use App\Models\Traits\Sluggable;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Schema;
use Tests\TestCase;
class SluggableTest extends TestCase
{
use RefreshDatabase;
/** @test */
public function add_slug_generates_a_new_slug()
{
$model = new SluggableStub(['title' => 'Test Add Slug']);
$model->addSlug();
$this->assertEquals('test-add-slug', $model->getAttribute('slug'));
}
/** @test */
public function add_slug_doesnt_do_anything_if_slug_field_is_dirty()
{
$model = new SluggableStub([
'title' => 'Random String',
'slug' => null,
]);
$model->setAttribute('slug', 'test-slug-dirty')->addSlug();
$this->assertEquals('test-slug-dirty', $model->getAttribute('slug'));
}
/** @test */
public function find_by_slug_returns_the_model()
{
$model = SluggableStub::create([
'title' => 'Test Find By Slug',
'slug' => 'test-find-by-slug',
]);
$record = $model::findBySlug('test-find-by-slug');
$this->assertInstanceOf(Model::class, $record);
$this->assertTrue($record->is($model));
}
/** @test */
public function find_by_slug_works_with_custom_save_to_column_name()
{
Schema::create('sluggable_test', function (Blueprint $table) {
$table->increments('id');
$table->string('title')->nullable();
$table->string('custom_slug_column');
$table->timestamps();
});
$model = new class extends Model {
use Sluggable;
protected $table = 'sluggable_test';
protected $sluggable = ['save_to' => 'custom_slug_column'];
protected $attributes = [
'title' => 'Test find by custom slug column',
'custom_slug_column' => 'test-find-by-custom-slug-column',
];
};
$model->save();
$record = $model::findBySlug('test-find-by-custom-slug-column');
$this->assertInstanceOf(Model::class, $record);
$this->assertTrue($record->is($model));
}
/** @test */
public function it_generates_a_unique_slug_before_creating()
{
$model = new SluggableStub(['title' => 'Test Creating Title']);
$model->save();
$this->assertDatabaseHas($model->getTable(), [
'title' => 'Test Creating Title',
'slug' => 'test-creating-title'
]);
}
/** @test */
public function it_updates_the_slug_before_updating()
{
$model = new SluggableStub(['title' => 'Test Title']);
$model->save();
$model->update(['title' => 'Test Updated Title']);
$this->assertDatabaseHas($model->getTable(), [
'title' => 'Test Updated Title',
'slug' => 'test-updated-title'
]);
}
/** @test */
public function it_returns_null_when_find_by_slug_does_not_exist()
{
$result = SluggableStub::findBySlug('test-non-existent');
$this->assertNull($result);
}
/** @test */
public function it_defaults_the_build_slug_from_column_to_title()
{
$this->assertEquals('title', (new SluggableStub)->buildSlugFrom());
}
/** @test */
public function it_allows_to_override_a_method_to_indicate_from_which_column_to_generate_the_slug()
{
$model = new class extends Model {
use Sluggable;
public function buildSlugFrom() : string
{
return 'custom_build_from_column';
}
};
$this->assertEquals('custom_build_from_column', $model->buildSlugFrom());
}
/** @test */
public function it_defaults_the_save_to_column_to_slug()
{
$this->assertEquals('slug', (new SluggableStub)->saveSlugTo());
}
/** @test */
public function it_allows_to_override_a_method_to_indicate_to_which_column_the_slug_should_be_saved()
{
$model = new class extends Model {
use Sluggable;
public function saveSlugTo() : string
{
return 'custom_save_to_column';
}
};
$this->assertEquals('custom_save_to_column', $model->saveSlugTo());
}
/** @test */
public function it_allows_models_to_define_a_property_to_indicate_the_build_and_save_columns()
{
$model = new class extends Model {
use Sluggable;
protected $sluggable = [
'build_from' => 'custom_build_from_column',
'save_to' => 'custom_save_to_column',
];
};
$this->assertEquals('custom_build_from_column', $model->buildSlugFrom());
$this->assertEquals('custom_save_to_column', $model->saveSlugTo());
}
/**
* Refresh the in-memory database.
*
* @override \Illuminate\Foundation\Testing\RefreshDatabase
*
* @return void
*/
protected function refreshDatabase()
{
Schema::create('stubs', function (Blueprint $table) {
$table->increments('id');
$table->string('title');
$table->string('slug');
$table->timestamps();
});
$this->app[Kernel::class]->setArtisan(null);
}
}
class SluggableStub extends Model
{
use Sluggable;
protected $table = 'stubs';
protected static $unguarded = true;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment