Last active
September 15, 2021 18:57
-
-
Save scrubmx/9bbdcf31ea574169a964a6c98a842960 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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