Skip to content

Commit

Permalink
Merge pull request #213 from marvin-wtt/implicit-model-binding
Browse files Browse the repository at this point in the history
Adds support for implicit route model binding with translated slugs
  • Loading branch information
freekmurze authored Dec 15, 2021
2 parents 8a6fc58 + 1b0f879 commit 6eabac0
Show file tree
Hide file tree
Showing 9 changed files with 230 additions and 15 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
fail-fast: false
matrix:
php: [8.1, 8.0]
laravel: [8.*]
laravel: [8.76.1]
dependency-version: [prefer-lowest, prefer-stable]

name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }}
Expand Down
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,8 +338,50 @@ class YourEloquentModel extends Model
->saveSlugsTo('slug');
}
}

```

#### Implicit route model binding

You can also use Laravels [implicit route model binding](https://laravel.com/docs/8.x/routing#implicit-binding) inside your controller to automatically resolve the model. To use this feature, make sure that the slug column matches the `routeNameKey`.
Currently, only some database types support JSON opterations. Further information about which databases support JSON can be found in the [Laravel docs](https://laravel.com/docs/8.x/queries#json-where-clauses).

```php
namespace App;

use Spatie\Sluggable\HasSlug;
use Spatie\Sluggable\SlugOptions;
use Illuminate\Database\Eloquent\Model;

class YourEloquentModel extends Model
{
use HasTranslations, HasTranslatableSlug;

public $translatable = ['name', 'slug'];

/**
* Get the options for generating the slug.
*/
public function getSlugOptions() : SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom('name')
->saveSlugsTo('slug');
}

/**
* Get the route key for the model.
*
* @return string
*/
public function getRouteKeyName()
{
return 'slug';
}
}
```


## Changelog

Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently.
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
],
"require": {
"php": "^8.0",
"illuminate/database": "^8.0",
"illuminate/database": "^8.76.1",
"illuminate/support": "^8.0"
},
"require-dev": {
Expand Down
15 changes: 14 additions & 1 deletion src/HasTranslatableSlug.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Spatie\Sluggable;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\Localizable;
Expand Down Expand Up @@ -94,7 +96,7 @@ protected function slugIsBasedOnTitle(): bool
$slugSeparator = $currentSlug[strlen($titleSlug)];
$slugIdentifier = substr($currentSlug, strlen($titleSlug) + 1);

return $slugSeparator === $this->slugOptions->slugSeparator && is_numeric($slugIdentifier);
return $slugSeparator === $this->slugOptions->slugSeparator && is_numeric($slugIdentifier);
}

protected function getOriginalSourceString(): string
Expand All @@ -120,4 +122,15 @@ protected function hasCustomSlugBeenUsed(): bool

return $originalSlug !== $newSlug;
}

public function resolveRouteBindingQuery($query, $value, $field = null): Builder|Relation
{
$field = $field ?? $this->getRouteKeyName();

if ($field !== $this->getSlugOptions()->slugField) {
return parent::resolveRouteBindingQuery($query, $value, $field);
}

return $query->where("{$field}->{$this->getLocale()}", $value);
}
}
100 changes: 100 additions & 0 deletions tests/HasTranslatableSlugTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Spatie\Sluggable\Tests;

use Illuminate\Support\Facades\Route;
use Spatie\Sluggable\SlugOptions;

class HasTranslatableSlugTest extends TestCase
Expand Down Expand Up @@ -302,4 +303,103 @@ public function it_can_update_slug_with_non_unique_names_multiple()
$this->assertSame($expectedSlug, $model->getTranslation('slug', 'en'));
}
}

/** @test */
public function it_can_resolve_route_binding()
{
$model = new TranslatableModel();

$model->setTranslation('name', 'en', 'Test value EN');
$model->setTranslation('name', 'nl', 'Test value NL');
$model->setTranslation('slug', 'en', 'updated-value-en');
$model->setTranslation('slug', 'nl', 'updated-value-nl');
$model->save();

// Test for en locale
$result = (new TranslatableModel())->resolveRouteBinding('updated-value-en', 'slug');

$this->assertNotNull($result);
$this->assertEquals($model->id, $result->id);

// Test for nl locale
$this->app->setLocale('nl');

$result = (new TranslatableModel())->resolveRouteBinding('updated-value-nl', 'slug');

$this->assertNotNull($result);
$this->assertEquals($model->id, $result->id);

// Test for fr locale - should fail
$this->app->setLocale('fr');
$result = (new TranslatableModel())->resolveRouteBinding('updated-value-nl', 'slug');
$this->assertNull($result);
}

/** @test */
public function it_can_resolve_route_binding_even_when_soft_deletes_are_on()
{
foreach (range(1, 10) as $i) {
$model = new TranslatableModelSoftDeletes();
$model->setTranslation('name', 'en', 'Test value EN');
$model->setTranslation('slug', 'en', 'updated-value-en-' . $i);
$model->save();
$model->delete();

$result = (new TranslatableModelSoftDeletes())->resolveSoftDeletableRouteBinding(
'updated-value-en-' . $i,
'slug'
);

$this->assertNotNull($result);
$this->assertEquals($model->id, $result->id);
}
}
/** @test */
public function it_can_bind_route_model_implicit()
{
$model = new TranslatableModel();
$model->setTranslation('name', 'en', 'Test value EN');
$model->setTranslation('slug', 'en', 'updated-value-en');
$model->save();

Route::get(
'/translatable-model/{test:slug}',
function (TranslatableModel $test) use ($model) {
$this->assertNotNull($test);
$this->assertEquals($model->id, $test->id);
}
)->middleware('bindings');

$response = $this->get("/translatable-model/updated-value-en");

$response->assertStatus(200);
}

/** @test */
public function it_can_bind_child_route_model_implicit()
{
$model = new TranslatableModel();
$model->setTranslation('name', 'en', 'Test value EN');
$model->setTranslation('slug', 'en', 'updated-value-en');
$model->test_model_id = 1;
$model->save();

$parent = new TestModel();
$parent->name = 'parent';
$parent->save();

Route::get(
'/test-model/{test_model:url}/translatable-model/{translatable_model:slug}',
function (TestModel $testModel, TranslatableModel $translatableModel) use ($parent, $model) {
$this->assertNotNull($parent);
$this->assertNotNull($translatableModel);
$this->assertEquals($parent->id, $testModel->id);
$this->assertEquals($model->id, $translatableModel->id);
}
)->middleware('bindings');

$response = $this->get("/test-model/parent/translatable-model/updated-value-en");

$response->assertStatus(200);
}
}
31 changes: 21 additions & 10 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use File;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Schema;
use Orchestra\Testbench\TestCase as Orchestra;

abstract class TestCase extends Orchestra
Expand All @@ -26,45 +27,55 @@ protected function getEnvironmentSetUp($app)
{
$this->initializeDirectory($this->getTempDirectory());

$app['config']->set('database.default', 'sqlite');
$app['config']->set('database.connections.sqlite', [
config()->set('database.default', 'sqlite');
config()->set('database.connections.sqlite', [
'driver' => 'sqlite',
'database' => $this->getTempDirectory().'/database.sqlite',
'database' => $this->getTempDirectory() . '/database.sqlite',
'prefix' => '',
]);
}

/**
* @param $app
* @param Application $app
*/
protected function setUpDatabase(Application $app)
{
file_put_contents($this->getTempDirectory().'/database.sqlite', null);
file_put_contents($this->getTempDirectory() . '/database.sqlite', null);

$app['db']->connection()->getSchemaBuilder()->create('test_models', function (Blueprint $table) {
Schema::create('test_models', function (Blueprint $table) {
$table->increments('id');
$table->string('name')->nullable();
$table->string('other_field')->nullable();
$table->string('url')->nullable();
});

$app['db']->connection()->getSchemaBuilder()->create('test_model_soft_deletes', function (Blueprint $table) {
Schema::create('test_model_soft_deletes', function (Blueprint $table) {
$table->increments('id');
$table->string('name')->nullable();
$table->string('other_field')->nullable();
$table->string('url')->nullable();
$table->softDeletes();
});

$app['db']->connection()->getSchemaBuilder()->create('translatable_models', function (Blueprint $table) {
Schema::create('translatable_models', function (Blueprint $table) {
$table->increments('id');
$table->text('name')->nullable();
$table->text('other_field')->nullable();
$table->text('non_translatable_field')->nullable();
$table->text('slug')->nullable();
$table->foreignId('test_model_id')->nullable()->index();
});

Schema::create('translatable_model_soft_deletes', function (Blueprint $table) {
$table->increments('id');
$table->text('name')->nullable();
$table->text('other_field')->nullable();
$table->text('non_translatable_field')->nullable();
$table->text('slug')->nullable();
$table->softDeletes();
});

$app['db']->connection()->getSchemaBuilder()->create('scopeable_models', function (Blueprint $table) {
Schema::create('scopeable_models', function (Blueprint $table) {
$table->increments('id');
$table->text('name')->nullable();
$table->text('slug')->nullable();
Expand All @@ -82,6 +93,6 @@ protected function initializeDirectory(string $directory)

public function getTempDirectory(): string
{
return __DIR__.'/temp';
return __DIR__ . '/temp';
}
}
6 changes: 6 additions & 0 deletions tests/TestModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Spatie\Sluggable\Tests;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Spatie\Sluggable\HasSlug;
use Spatie\Sluggable\SlugOptions;

Expand Down Expand Up @@ -43,4 +44,9 @@ public function getDefaultSlugOptions(): SlugOptions
->generateSlugsFrom('name')
->saveSlugsTo('url');
}

public function translatableModels(): HasMany
{
return $this->hasMany(TranslatableModel::class);
}
}
10 changes: 8 additions & 2 deletions tests/TranslatableModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Spatie\Sluggable\Tests;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Spatie\Sluggable\HasTranslatableSlug;
use Spatie\Sluggable\SlugOptions;
use Spatie\Translatable\HasTranslations;
Expand All @@ -17,9 +18,9 @@ class TranslatableModel extends Model
protected $guarded = [];
public $timestamps = false;

public $translatable = ['name', 'other_field', 'slug'];
protected $translatable = ['name', 'other_field', 'slug'];

private $customSlugOptions;
protected $customSlugOptions;

public function useSlugOptions($slugOptions)
{
Expand All @@ -32,4 +33,9 @@ public function getSlugOptions(): SlugOptions
->generateSlugsFrom('name')
->saveSlugsTo('slug');
}

public function testModel(): BelongsTo
{
return $this->belongsTo(TestModel::class);
}
}
37 changes: 37 additions & 0 deletions tests/TranslatableModelSoftDeletes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace Spatie\Sluggable\Tests;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Spatie\Sluggable\HasTranslatableSlug;
use Spatie\Sluggable\SlugOptions;
use Spatie\Translatable\HasTranslations;

class TranslatableModelSoftDeletes extends Model
{
use HasTranslations;
use HasTranslatableSlug;
use SoftDeletes;

protected $table = 'translatable_model_soft_deletes';

protected $guarded = [];
public $timestamps = false;

protected $translatable = ['name', 'other_field', 'slug'];

protected $customSlugOptions;

public function useSlugOptions($slugOptions)
{
$this->customSlugOptions = $slugOptions;
}

public function getSlugOptions(): SlugOptions
{
return $this->customSlugOptions ?: SlugOptions::create()
->generateSlugsFrom('name')
->saveSlugsTo('slug');
}
}

0 comments on commit 6eabac0

Please sign in to comment.