Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds support for implicit route model binding with translated slugs #213

Merged
merged 24 commits into from
Dec 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2926f16
Adds support for implicit route model binding with translated slugs
marvin-wtt Nov 11, 2021
58659f3
Fixes runtime exception being thrown when database doesn't support js…
marvin-wtt Nov 11, 2021
fdd9662
Updates README.md
marvin-wtt Nov 11, 2021
a955f76
Fixed style issue
marvin-wtt Nov 11, 2021
fe4189c
Adds support for SQLite databases
marvin-wtt Nov 14, 2021
2618a5a
Adds tests
marvin-wtt Nov 14, 2021
4634512
Update README.md
freekmurze Nov 15, 2021
d127805
Adds full route test
marvin-wtt Nov 15, 2021
e099c13
Merge remote-tracking branch 'origin/implicit-model-binding' into imp…
marvin-wtt Nov 15, 2021
23b2800
Updates style
marvin-wtt Nov 15, 2021
10fb7ee
Updates style
marvin-wtt Nov 15, 2021
c3e081e
Merge remote-tracking branch 'origin/implicit-model-binding' into imp…
marvin-wtt Nov 15, 2021
e232cbd
Update README.md
freekmurze Nov 16, 2021
f1a74d9
Update README.md
freekmurze Nov 16, 2021
f3a06c6
Updates database setup to Facades
marvin-wtt Nov 17, 2021
21ade83
Merge remote-tracking branch 'origin/implicit-model-binding' into imp…
marvin-wtt Nov 17, 2021
57a5acc
Updates style
marvin-wtt Nov 21, 2021
cf73d38
Update HasTranslatableSlug.php
freekmurze Nov 22, 2021
b055b63
Update README.md
marvin-wtt Nov 22, 2021
da14983
Fix child model binding resolution
marvin-wtt Dec 9, 2021
04412c9
Update README
marvin-wtt Dec 9, 2021
bc08206
Update test syntax
marvin-wtt Dec 10, 2021
2207cf8
Update laravel version
marvin-wtt Dec 14, 2021
1b0f879
Merge branch 'main' into implicit-model-binding
freekmurze Dec 15, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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`.
marvin-wtt marked this conversation as resolved.
Show resolved Hide resolved
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",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sail@f34348e1ecf9:/var/www/html$ composer require spatie/laravel-sluggable
Using version ^3.2 for spatie/laravel-sluggable
./composer.json has been updated
Running composer update spatie/laravel-sluggable
Loading composer repositories with package information
Updating dependencies
Your requirements could not be resolved to an installable set of packages.

  Problem 1
    - Root composer.json requires spatie/laravel-sluggable ^3.2 -> satisfiable by spatie/laravel-sluggable[3.2.0].
    - spatie/laravel-sluggable 3.2.0 requires illuminate/database ^8.76.1 -> found illuminate/database[v8.76.1, ..., 8.x-dev] but these were not loaded, likely because it conflicts with another require.


Installation failed, reverting ./composer.json and ./composer.lock to their original content.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What other packages do you use? And which version do you use for your project?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image
I was able to install only after updating all packages

"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();
marvin-wtt marked this conversation as resolved.
Show resolved Hide resolved

$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');
marvin-wtt marked this conversation as resolved.
Show resolved Hide resolved

$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');
}
}