Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Revisit the full slug saving flow
Browse files Browse the repository at this point in the history
Tofandel committed Nov 21, 2024
1 parent 53f59c3 commit 9521fd6
Showing 21 changed files with 382 additions and 157 deletions.
Original file line number Diff line number Diff line change
@@ -168,7 +168,7 @@ class Page extends Model
'description',
];
public $slugAttributes = [
protected $slugFields = [
'title',
];
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ class Article extends Model implements LocalizedUrlRoutable
'description',
];

public $slugAttributes = [
protected $slugFields = [
'title',
];

Original file line number Diff line number Diff line change
@@ -57,7 +57,7 @@ class Article extends Model implements Sortable

// #endregion fillable

public $slugAttributes = [
private $slugFields = [
'title',
];

2 changes: 1 addition & 1 deletion examples/basic-page-builder/app/Models/Page.php
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ class Page extends Model
'description',
];

public $slugAttributes = [
protected $slugFields = [
'title',
];
}
2 changes: 1 addition & 1 deletion examples/portfolio/app/Models/Partner.php
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ class Partner extends Model
'description',
];

public $slugAttributes = [
protected $slugFields = [
'title',
];

2 changes: 1 addition & 1 deletion examples/portfolio/app/Models/Project.php
Original file line number Diff line number Diff line change
@@ -27,7 +27,7 @@ class Project extends Model
'description',
];

public $slugAttributes = [
protected $slugFields = [
'title',
];

Original file line number Diff line number Diff line change
@@ -26,7 +26,7 @@ class Post extends Model implements Sortable

public $translatedAttributes = ['title', 'description'];

public $slugAttributes = ['title'];
public $slugFields = ['title'];

public $mediasParams = [
'cover' => [
2 changes: 1 addition & 1 deletion examples/tests-modules/app/Models/Author.php
Original file line number Diff line number Diff line change
@@ -56,7 +56,7 @@ class Author extends Model implements Sortable
public $translatedAttributes = ['name', 'description', 'bio'];

// uncomment and modify this as needed if you use the HasSlug trait
public $slugAttributes = ['name'];
protected $slugFields = ['name'];

// add checkbox fields names here (published toggle is itself a checkbox)
public $checkboxes = ['published'];
2 changes: 1 addition & 1 deletion examples/tests-modules/app/Models/Category.php
Original file line number Diff line number Diff line change
@@ -25,7 +25,7 @@ class Category extends Model
public $translatedAttributes = ['title'];

// uncomment and modify this as needed if you use the HasSlug trait
public $slugAttributes = ['title'];
protected $slugFields = ['title'];

// add checkbox fields names here (published toggle is itself a checkbox)
public $checkboxes = ['published'];
2 changes: 1 addition & 1 deletion examples/tests-singleton/app/Models/ContactPage.php
Original file line number Diff line number Diff line change
@@ -25,7 +25,7 @@ class ContactPage extends Model
'description',
];

public $slugAttributes = [
protected $slugFields = [
'title',
];

2 changes: 1 addition & 1 deletion examples/tests-subdomain-routing/app/Models/Page.php
Original file line number Diff line number Diff line change
@@ -28,7 +28,7 @@ class Page extends Model implements Sortable
'description',
];

public $slugAttributes = [
public $slugFields = [
'title',
];

3 changes: 2 additions & 1 deletion src/Commands/stubs/model.stub
Original file line number Diff line number Diff line change
@@ -21,8 +21,9 @@ class {{modelClassName}} extends Model {{modelImplements}}
'description',
];
{{/hasTranslation}}{{hasSlug}}
public $slugAttributes = [
protected $slugFields = [
'title',
];
protected $slugDeps = [];
{{/hasSlug}}
}
229 changes: 146 additions & 83 deletions src/Models/Behaviors/HasSlug.php
Original file line number Diff line number Diff line change
@@ -5,14 +5,16 @@
use A17\Twill\Facades\TwillCapsules;
use A17\Twill\Models\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;

/** @property Collection<Model> $slugs */
trait HasSlug
{
private int $nb_variation_slug = 3;
public array $twillSlugData = [];
public ?array $twillSlugData = null;

private bool $twill_restoring = false;

@@ -23,9 +25,17 @@ protected static function bootHasSlug(): void
$model->twill_restoring = true;
});

static::saving(function (self $model) {
if (!$model->twill_restoring && !isset($model->twillSlugData)) {
// Run this before saving because we need to know which fields are dirty
$model->twillSlugData = $model->getSlugParams(null, true);
}
});

static::saved(function (self $model) {
if (!$model->twill_restoring) {
$model->handleSlugsOnSave();
$model->handleSlugsSaving();
$model->twillSlugData = null;
}
$model->twill_restoring = false;
});
@@ -119,28 +129,28 @@ public function restoreSlugs(): void
}

/**
* When a new model is created there is more than one language, we generate the slugs where there is no locale
* variant yet based on the source.
* @deprecated This method should not be used directly and will be removed in 4.x use $model->save() instead
*/
public function handleSlugsOnSave(): void
{
$this->disableLocaleSlugs();
$this->handleSlugsSaving();
}

$slugParams = $this->twillSlugData !== [] ? $this->twillSlugData : $this->getSlugParams();
private function handleSlugsSaving(): void
{
if (!isset($this->twillSlugData)) {
$slugParams = $this->getSlugParams(null, true);
} else {
$slugParams = $this->twillSlugData;

foreach ($slugParams as $params) {
if (in_array($params['locale'], config('twill.slug_utf8_languages', []))) {
$params['slug'] = $this->getUtf8Slug($params['slug']);
} else {
$params['slug'] = Str::slug($params['slug']);
foreach ($slugParams as $locale => $params) {
$slugParams[$locale] = array_merge($this->getSlugParams($locale, empty($params['slug'])) ?? [], $params);
}
$slugParams = array_filter($slugParams, fn($p) => !empty($p['slug']));
}

if (empty($params['slug'])) {
continue;
}
if ($this->slugs()->where('locale', $params['locale'])->where('slug', $params['slug'])->where('active', true)->doesntExist()) {
$this->updateOrNewSlug($params);
}
foreach ($slugParams as $params) {
$this->updateOrNewSlug($params);
}
}

@@ -152,28 +162,34 @@ public function updateOrNewSlug(array $slugParams): void
$slugParams['slug'] = Str::slug($slugParams['slug']);
}

if (empty($slugParams['slug'])) {
return;
}
$slugParams['slug'] = $this->suffixSlugIfExisting($slugParams);
$oldMatchingSlug = $this->getExistingSlug($slugParams, true);

// Active old slug if already existing or create a new one.
// The first attempt is to find one without a suffix, a second attempt is done with the suffix.
// If both matches none, we will go to the regular creation flow.
if (
(($oldSlug = $this->getExistingSlug($slugParams, true)) !== null)
&& ($slugParams['slug'] === $this->suffixSlugIfExisting($slugParams))
) {
if (!$oldSlug->active && ($slugParams['active'] ?? false)) {
$this->getSlugModelClass()::where('id', $oldSlug->id)->update(['active' => 1]);
$this->disableLocaleSlugs($oldSlug->locale, $oldSlug->id);
}
} elseif (
$this->slugNeedsSuffix($slugParams) &&
(($oldSlug = $this->getExistingSlug($slugParams)) !== null) &&
($slugParams['slug'] === $this->suffixSlugIfExisting($slugParams))
) {
if (!$oldSlug->active && ($slugParams['active'] ?? false)) {
$this->getSlugModelClass()::where('id', $oldSlug->id)->update(['active' => 1]);
$this->disableLocaleSlugs($oldSlug->locale, $oldSlug->id);
if ($oldMatchingSlug) {
$isNowActive = (bool)($slugParams['active'] ?? false);
if ($oldMatchingSlug->active != $isNowActive) {
$this->slugs()->whereKey($oldMatchingSlug->getKey())->update(['active' => $isNowActive]);
if ($this->relationLoaded('slugs')) {
// Report update to slugs so that getSlug() returns the correct value
$slug = $this->slugs->whereKey($oldMatchingSlug->getKey());
if ($slug) {
$slug->active = $isNowActive;
$slug->syncOriginalAttribute('active');
} else {
// The relation was loaded before the old slug even existed, unload it and let it lazy reload as needed
$this->unsetRelation('slugs');
}
}
if ($isNowActive) {
$this->disableLocaleSlugs($oldMatchingSlug->locale, $oldMatchingSlug->getKey());
}
}
} else {
$this->addOneSlug($slugParams);
$this->addOneSlug($slugParams, true);
}
}

@@ -204,44 +220,58 @@ public function getExistingSlug(array $slugParams, bool $forRecreate = false): ?
return $query->first();
}

protected function addOneSlug(array $slugParams): void
protected function addOneSlug(array $slugParams, $alreadySuffixed = false): void
{
$datas = [];
foreach ($slugParams as $key => $value) {
$datas[$key] = $value;
if (!$alreadySuffixed) {
$slugParams['slug'] = $this->suffixSlugIfExisting($slugParams);
}

$datas['slug'] = $this->suffixSlugIfExisting($slugParams);

$datas[$this->getForeignKey()] = $this->id;
$slugModel = \Illuminate\Database\Eloquent\Model::unguarded(fn() => $this->slugs()->create($slugParams));

$slugModel = \Illuminate\Database\Eloquent\Model::unguarded(fn () => $this->getSlugModelClass()::create($datas));

$this->disableLocaleSlugs($slugParams['locale'], $slugModel->getKey());
if ($this->relationLoaded('slugs')) {
// Report update to slugs so that getSlug() returns the correct value
$this->slugs->add($slugModel);
}
if (!$this->wasRecentlyCreated) {
// There will not be any old slug if the model was just created
$this->disableLocaleSlugs($slugParams['locale'], $slugModel->getKey());
}
}

public function disableLocaleSlugs(string|array $locale = null, int $except_slug_id = 0): void
public function disableLocaleSlugs(string|array $locale = null, int|array $except_slug_id = 0): void
{
$query = $this->getSlugModelClass()::where($this->getForeignKey(), $this->id)
->where('id', '<>', $except_slug_id);
$query = $this->slugs()
->where('active', true)
->whereNotIn('id', Arr::wrap($except_slug_id));
if ($locale !== null) {
$query->whereIn('locale', Arr::wrap($locale));
}
$query->update(['active' => 0]);
if ($this->relationLoaded('slugs')) {
// Report update to slugs so that getSlug() returns the correct value after an update without needing a refresh
$this->slugs->where('active', true)
->whereNotIn('id', Arr::wrap($except_slug_id))
->whereIn('locale', Arr::wrap($locale))
->each(function (Model $slug) {
$slug->active = false;
$slug->syncOriginalAttribute('active');
});
}
}

private function suffixSlugIfExisting(array $slugParams): string
{
$idsToExclude = $this->slugs()->withTrashed()->get('id')->pluck('id', 'id')->all();

$slugBackup = $slugParams['slug'];

unset($slugParams['active']);


for ($i = 2; $i <= $this->nb_variation_slug + 1; ++$i) {
/** @var Builder $qCheck */
$qCheck = $this->getSlugModelClass()::query();
$qCheck->whereNull($this->getDeletedAtColumn());
$qCheck->whereNotIn('id', $idsToExclude);
$qCheck->whereNot($this->getForeignKey(), $this->getKey());

foreach ($slugParams as $key => $value) {
$qCheck->where($key, '=', $value);
}
@@ -259,10 +289,13 @@ private function suffixSlugIfExisting(array $slugParams): string
}

/**
* @deprecated use suffixSlugIfExisting instead
* Checks if a slug needs a suffix due to a conflict with another model.
*/
private function slugNeedsSuffix(array $slugParams): bool
{
trigger_deprecation('area17/twill', '3.5', 'The slugNeedsSuffix method is deprecated and will be removed in 4.x use suffixSlugIfExisting instead');

unset($slugParams['active']);

$hasExisting = false;
@@ -325,58 +358,88 @@ public function getSlugAttribute(): string
return $this->getSlug();
}

public function getSlugParams(?string $locale = null): ?array
public function getSlugDeps(): array
{
if (!isset($this->translations) || count(getLocales()) === 1 || $this->translations->isEmpty()) {
$slugParams = $this->getSingleSlugParams($locale);
if ($slugParams !== null && !empty($slugParams)) {
return $slugParams;
}
return $this->slugDeps ?? array_slice($this->slugAttributes ?? [], 1);
}
public function getSlugFields(): array
{
if (!isset($this->slugFields) && isset($this->slugAttributes)) {
trigger_deprecation('area17/twill', '3.5', 'The slugAttributes property has been deprecated instead define slug fields with the slugFields property and additional columns with the slugDeps property');
}
return $this->slugFields ?? array_slice($this->slugAttributes ?? [], 0, 1);
}

public function getSlugParams(?string $locale = null, bool $skipUnchanged = false): ?array
{
$translatedAttributes = $this->getTranslatedAttributes();
$slugParams = [];
foreach ($this->translations as $translation) {
if ($translation->locale === $locale || $locale === null) {
$attributes = $this->slugAttributes;

if (!$attributes) {
continue;
}

$slugAttribute = array_shift($attributes);
$deps = $this->getSlugDeps();
$fields = $this->getSlugFields();
if (empty($fields)) {
return $locale === null ? [] : null;
}
foreach ($locale ? [$locale] : getLocales() as $appLocale) {
if ($appLocale === $locale || $locale === null) {
$wasChanged = $this->wasRecentlyCreated;

$slugDependenciesAttributes = [];
foreach ($attributes as $attribute) {
if (!isset($this->$attribute)) {
throw new \Exception("You must define the field {$attribute} in your model");
$translation = $this->translate($appLocale, $this->usePropertyFallback());
$getAttributeValue = function ($attribute) use ($translatedAttributes, $appLocale, $translation, &$wasChanged) {
if (in_array($attribute, $translatedAttributes)) {
if (!$wasChanged && $translation?->isDirty($attribute)) {
$wasChanged = true;
}
if (!isset($translation->$attribute) && config('translatable.use_property_fallback', false)) {
$fallback = $this->getFallbackLocale($appLocale);
$fallbackTranslation = $this->translate($fallback);
if (!$wasChanged && $fallbackTranslation?->isDirty($attribute)) {
$wasChanged = true;
}
return $fallbackTranslation?->$attribute;
}
return $translation?->$attribute;
}
if (!$wasChanged && $this->isDirty($attribute)) {
$wasChanged = true;
}
return $this->$attribute;
};

$slugDependenciesAttributes[$attribute] = $this->$attribute;
}
$slugFields = array_filter(array_map($getAttributeValue, $fields));

if (!isset($translation->$slugAttribute) && !isset($this->$slugAttribute)) {
throw new \Exception("You must define the field {$slugAttribute} in your model");
if (empty($slugFields)) {
// Skip empty slugs
continue;
}
$slugDependencies = array_filter(array_combine($deps, array_map($getAttributeValue, $deps)));
if (count($slugDependencies) !== count($deps)) {
$missing = array_keys(array_diff_key(array_flip($deps), $slugDependencies));
throw new \Exception('The slug dependencies ' . (Arr::join($missing, ', ', ' and ') . ' are missing'));
}

$slugParam = [
'active' => $translation->active ?? true,
'slug' => $translation->$slugAttribute ?? $this->$slugAttribute,
'locale' => $translation->locale,
] + $slugDependenciesAttributes;
'active' => $translation?->active ?? true,
'slug' => Arr::join($slugFields, '-'),
'locale' => $appLocale,
] + $slugDependencies;

if ($locale != null) {
return $slugParam;
return !$skipUnchanged || $wasChanged ? $slugParam : null;
}

$slugParams[] = $slugParam;
if (!$skipUnchanged || $wasChanged) {
$slugParams[$appLocale] = $slugParam;
}
}
}

return $locale === null ? $slugParams : null;
}

/** @deprecated */
public function getSingleSlugParams(?string $locale = null): ?array
{
trigger_deprecation('area17/twill', '3.5', 'The getSingleSlugParams method is deprecated and will be removed in 4.x as it is not used, use getSlugParams instead');

$slugParams = [];
foreach (getLocales() as $appLocale) {
if ($appLocale === $locale || $locale === null) {
@@ -433,7 +496,7 @@ public function getForeignKey(): string

protected function getSuffixSlug(): string|int
{
return $this->id;
return $this->getKey();
}

/**
11 changes: 11 additions & 0 deletions src/Models/Model.php
Original file line number Diff line number Diff line change
@@ -28,6 +28,17 @@ abstract class Model extends BaseModel implements TaggableInterface, TwillModelC

public $timestamps = true;

public static function boot(): void
{
static::saving(function (self $model) {
// When saving a model multiple times in a row without refresh, then the model should not be recently created anymore
if ($model->wasRecentlyCreated) {
$model->wasRecentlyCreated = false;
}
});
parent::boot();
}

protected function isTranslationModel(): bool
{
return Str::endsWith(get_class($this), 'Translation');
2 changes: 1 addition & 1 deletion src/Repositories/Behaviors/HandleBlocks.php
Original file line number Diff line number Diff line change
@@ -109,7 +109,7 @@ public function afterSaveHandleBlocks(Model $object, array $fields): void
$this->validateBlockArray($block, $blockInstance, $handleTranslations);
}

$existingBlockIds = $object->blocks()->pluck('id')->toArray();
$existingBlockIds = $object->wasRecentlyCreated ? [] : $object->blocks()->pluck('id')->toArray();

$usedBlockIds = [];

50 changes: 23 additions & 27 deletions src/Repositories/Behaviors/HandleSlugs.php
Original file line number Diff line number Diff line change
@@ -11,38 +11,32 @@ trait HandleSlugs
{
public function beforeSaveHandleSlugs(TwillModelContract $object, array $fields): void
{
if (property_exists($this->model, 'slugAttributes')) {
if (method_exists($this->model, 'getSlugFields')) {
$object->twillSlugData = [];
$submittedLanguages = Collection::make($fields['languages'] ?? []);

foreach (getLocales() as $locale) {
$submittedLanguage = Arr::first($submittedLanguages->filter(function ($lang) use ($locale) {
$atLeastOneLanguageIsPublished = $submittedLanguages->contains(function ($language) {
return $language['published'];
});
foreach (getLocales() as $index => $locale) {
$submittedLanguage = $submittedLanguages->first(function ($lang) use ($locale) {
return $lang['value'] === $locale;
}));
});

if (isset($fields['slug'][$locale]) && !empty($fields['slug'][$locale])) {
$currentSlug = [];
$shouldPublishFirstLanguage = ($index === 0 && !$atLeastOneLanguageIsPublished);

$fallBack = $fields[$locale]['active'] ?? false;

// Copy active fallback behavior from HandleTranslations
$activeField = $shouldPublishFirstLanguage || ($submittedLanguage['published'] ?? $fallBack);

$currentSlug = [];
$currentSlug['locale'] = $locale;
$currentSlug['active'] = $activeField;
if (!empty($fields['slug'][$locale])) {
$currentSlug['slug'] = $fields['slug'][$locale];
$currentSlug['locale'] = $locale;
$currentSlug['active'] = $submittedLanguage['published'] ?? true;
$currentSlug = $this->getSlugParameters($object, $fields, $currentSlug);
$object->twillSlugData[] = $currentSlug;
} else {
$slugParams = $this->model->slugAttributes;
$slugData = [];

foreach ($slugParams as $param) {
$slugData[] = $fields[$param][$locale] ?? '';
}

if (!empty(Arr::join($slugData, '-'))) {
$object->twillSlugData[] = [
'slug' => Str::slug(Arr::join($slugData, '-')),
'active' => $submittedLanguage['published'] ?? 1,
'locale' => $locale
];
}
}
$object->twillSlugData[$locale] = $currentSlug;
}
}
}
@@ -62,8 +56,11 @@ public function getFormFieldsHandleSlugs(TwillModelContract $model, array $field
return $fields;
}

public function getSlugParameters(TwillModelContract $object, array $fields, array $slug): array
/** @deprecated We merge twillSlugData with getSlugParams on save, to avoid getting outdated data */
public function getSlugParameters(TwillModelContract $object, array $fields, array $slug): ?array
{
trigger_deprecation('area17/twill', '3.5', 'The getSlugParameters method is deprecated as it returns data before fields are applied and will be removed in 4.x');

$slugParams = $object->getSlugParams($slug['locale']);

foreach ($object->slugAttributes as $param) {
@@ -73,7 +70,6 @@ public function getSlugParameters(TwillModelContract $object, array $fields, arr
$slug[$param] = $slugParams[$param];
}
}

return $slug;
}

6 changes: 3 additions & 3 deletions src/Repositories/Behaviors/HandleTranslations.php
Original file line number Diff line number Diff line change
@@ -28,9 +28,9 @@ public function prepareFieldsBeforeSaveHandleTranslations(?TwillModelContract $o
});

foreach ($locales as $index => $locale) {
$submittedLanguage = Arr::first($submittedLanguages->filter(function ($lang) use ($locale) {
$submittedLanguage = $submittedLanguages->first(function ($lang) use ($locale) {
return $lang['value'] === $locale;
}));
});

$shouldPublishFirstLanguage = ($index === 0 && !$atLeastOneLanguageIsPublished);

@@ -52,7 +52,7 @@ public function prepareFieldsBeforeSaveHandleTranslations(?TwillModelContract $o
return [
$attribute => ($attributeValue[$locale] ?? $fields[$locale][$attribute] ?? null),
];
})->toArray();
})->all();
}

unset($fields['languages']);
34 changes: 34 additions & 0 deletions tests/integration/Anonymous/AnonymousModule.php
Original file line number Diff line number Diff line change
@@ -94,6 +94,12 @@ class AnonymousModule
/** @var string[] */
private array $slugAttributes = [];

/** @var string[] */
private array $slugFields = [];

/** @var string[] */
private array $slugDeps = [];

protected function __construct(public string $namePlural, public Application $app)
{
$this->classPrinter = new PsrPrinter();
@@ -203,6 +209,22 @@ public function withSlugAttributes(array $fields): self
return $this;
}


public function withSlugFields(array $fields): self
{
$this->slugFields = $fields;

return $this;
}

public function withSlugDeps(array $fields): self
{
$this->slugDeps = $fields;

return $this;
}


/**
* Boots the anonymous module and returns the model class.
*/
@@ -626,6 +648,18 @@ private function getModelClass(string $classNameWithNamespace): PhpNamespace
$this->slugAttributes
);
}
if ($this->slugDeps !== []) {
$class->addProperty(
'slugDeps',
$this->slugDeps
);
}
if ($this->slugFields !== []) {
$class->addProperty(
'slugFields',
$this->slugFields
);
}

foreach ($this->belongsToMany as $name => $target) {
$method = $class->addMethod($name);
85 changes: 85 additions & 0 deletions tests/integration/Models/StandaloneSlugTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

namespace A17\Twill\Tests\Integration\Models;

use A17\Twill\Tests\Integration\Anonymous\AnonymousModule;
use A17\Twill\Tests\Integration\ModulesTestBase;
use A17\Twill\Tests\Integration\TestCase;
use App\Models\Author;
use App\Repositories\AuthorRepository;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;

class StandaloneSlugTest extends ModulesTestBase
{
public function testSavingStandaloneSlug(): void
{
$allPublished = [['published' => true, 'value' => 'en'], ['published' => true, 'value' => 'fr'], ['published' => true, 'value' => 'pt-BR']];
$createAuthor = fn (): Author => $this->app->get(AuthorRepository::class)->create(['name' => ['en' => 'Test author', 'fr' => 'Test auteur', 'pt-BR' => 'Test author'], 'languages' => $allPublished]);

$author = $createAuthor();
$this->assertCount(3, $author->getSlugParams(null, true));
$this->assertEquals(3, $author->slugs()->count());
$this->assertEquals('test-author', $author->slug);
$this->assertEquals('test-auteur', $author->getSlug('fr'));
$this->assertEquals('test-author', $author->getSlug('pt-BR'));

app(AuthorRepository::class)
->create(['name' => ['en' => 'Random author to change id']]);

DB::enableQueryLog();
$author->save();
$log = DB::getRawQueryLog();
// Nothing changed, there should be no queries
$this->assertCount(0, $log);

DB::enableQueryLog();
$author->name = 'New test author';
$author->translate('fr')->name = 'Nouveau test auteur';
$author->save();
$log = DB::getRawQueryLog();
DB::flushQueryLog();

// There should be 2 select and 2 updates for slugs and 1 update for the locale per locale changed
$this->assertEquals(10, count($log));
$this->assertEquals('new-test-author', $author->getSlug('en'));
$this->assertEquals('nouveau-test-auteur', $author->getSlug('fr'));
// All queries combined should take less than 40ms (they usually take a total of 5ms, allow big range to avoid flaky test)
$this->assertLessThan(40, array_sum(Arr::pluck($log, 'time')));

$author2 = $createAuthor();
$this->assertEquals('test-author-2', $author2->slug);
$this->assertEquals('test-auteur-2', $author2->getSlug('fr'));
$author3 = $createAuthor();
$this->assertEquals('test-author-3', $author3->slug);
$this->assertEquals('test-auteur-3', $author3->getSlug('fr'));
$author4 = $createAuthor();
$this->assertEquals('test-author-'.$author4->id, $author4->slug);
$this->assertEquals('test-auteur-'.$author4->id, $author4->getSlug('fr'));
$author4 = $author4->fresh();
$author4->name = 'New author slug';
DB::flushQueryLog();
$author4->save();
$log2 = DB::getRawQueryLog();
// 2 slug existence check, 1 slug insert, 1 slug update, 1 translation update,
$this->assertCount(5, $log2);
$this->assertEquals(3, $author4->slugs()->whereActive(true)->count());
$this->assertEquals(1, $author4->slugs()->whereActive(false)->count());
$this->assertEquals('new-author-slug', $author4->slug);
$author2->delete();
DB::flushQueryLog();
$author4->name = 'Test author';
$author4->save();
$log3 = DB::getRawQueryLog();
$this->assertEquals(3, $author4->slugs()->whereActive(true)->count());
$this->assertEquals(2, $author4->slugs()->whereActive(false)->count());
// 3 selects, 1 insert, 2 updates
$this->assertCount(6, $log3);
$this->assertEquals('test-author-2', $author4->slug);
DB::disableQueryLog();

$this->assertEquals(3, $author->slugs()->whereActive(true)->count());
$this->assertEquals(5, $author->slugs()->count());
}

}
2 changes: 1 addition & 1 deletion tests/integration/Repositories/TagsHandlerTest.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

namespace integration\Repositories;
namespace A17\Twill\Tests\Integration\Repositories;

use A17\Twill\Tests\Integration\ModulesTestBase;
use App\Repositories\AuthorRepository;
95 changes: 65 additions & 30 deletions tests/integration/SlugTest.php
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<?php

namespace integration;
namespace A17\Twill\Tests\Integration;

use A17\Twill\Models\Behaviors\HasSlug;
use A17\Twill\Models\Model;
use A17\Twill\Tests\Integration\Anonymous\AnonymousModule;
use A17\Twill\Tests\Integration\TestCase;

class SlugTest extends TestCase
{
@@ -24,6 +25,26 @@ public function setUp(): void
->boot();
}

public function testMultipleSlugAttributes()
{
$module = AnonymousModule::make('usernames', $this->app)
->withFields([
'first_name' => [],
'last_name' => [],
])
->withSlugFields([
'last_name', 'first_name'
])
->boot();

$model = $module->getRepository()->create([
'first_name' => 'John',
'last_name' => 'Doe',
]);

$this->assertEquals('doe-john', $model->getSlug());
}

public function testBasicSlugModel(): void
{
$model = $this->module->getRepository()->create([
@@ -36,17 +57,21 @@ public function testBasicSlugModel(): void

public function testBasicSlugModelDuplicate(): void
{
$this->module->getRepository()->create([
'title' => 'Id increment',
'slug' => ['en' => 'Id increment'],
]);
for ($i = 0; $i < 10; $i++) {
$model = $this->module->getRepository()->create([
'title' => 'My title',
'slug' => ['en' => 'my-title'],
]);

$this->assertEquals($i === 0 ? 'my-title' : 'my-title-' . $i + 1, $model->getSlug());
$this->assertEquals($i === 0 ? 'my-title' : 'my-title-' . ($i > 2 ? $model->id : $i + 1), $model->getSlug());
}
}

public function testSlugLooping(): void
public function testReactivateSlug(): void
{
$model = $this->module->getRepository()->create([
'title' => 'My title',
@@ -55,16 +80,27 @@ public function testSlugLooping(): void

$this->assertEquals('my-title', $model->getSlug());

$this->module->getRepository()->update($model->id, ['title' => 'My title 2']);
$this->module->getRepository()->update($model->id, ['title' => 'My title updated']);

$this->assertEquals('my-title-2', $model->fresh()->getSlug());
$this->assertEquals('my-title-updated', $model->fresh()->getSlug());

$this->assertCount(2, $model->slugs()->get());
$activeSlug = $model->slugs()->where('active', true)->get();
$inactiveSlug = $model->slugs()->where('active', false)->get();
$this->assertEquals('my-title-updated', $activeSlug->first()->slug);
$this->assertEquals('my-title', $inactiveSlug->first()->slug);
$this->assertCount(1, $activeSlug);
$this->assertCount(1, $inactiveSlug);

$this->module->getRepository()->update($model->id, ['title' => 'My title']);

$this->assertEquals('my-title', $model->fresh()->getSlug());
$this->assertCount(2, $model->slugs()->get());

$activeSlug = $model->slugs()->where('active', true)->get();
$inactiveSlug = $model->slugs()->where('active', false)->get();
$this->assertEquals('my-title', $activeSlug->first()->slug);
$this->assertEquals('my-title-updated', $inactiveSlug->first()->slug);
$this->assertCount(1, $activeSlug);
$this->assertCount(1, $inactiveSlug);
}

public function testCanReuseSoftDeletedSlug(): void
@@ -79,6 +115,9 @@ public function testCanReuseSoftDeletedSlug(): void

$this->module->getRepository()->delete($model->id);

$this->assertEquals(1, $model->slugs()->onlyTrashed()->count());
$this->assertEquals(0, $model->slugs()->count());

// Create a new model after the delete.
$newModel = $this->module->getRepository()->create([
'title' => 'My title',
@@ -94,7 +133,7 @@ public function testCanReuseSoftDeletedSlug(): void
// Restore the deleted model.
$this->assertTrue($this->module->getRepository()->restore($model->id));

$model = $this->module->getModelClassName()::find($model->id);
$model = $model->fresh();

$this->assertCount(1, $model->slugs()->get());
$this->assertEquals('my-title-2', $model->getSlug());
@@ -117,49 +156,45 @@ public function testCanReuseSoftDeletedSlugWithHistory(): void
$this->module->getRepository()->delete($model->id);

// Create a new model after the delete.
$newModel = $this->module->getRepository()->create([
$this->module->getRepository()->create([
'title' => 'My title',
'slug' => ['en' => 'slug-update'],
]);

// Total slugs should be 3.
$this->assertEquals('my-title', $this->module->getSlugModelClassName()::withTrashed()->get()[0]->slug);
$this->assertEquals('slug-update', $this->module->getSlugModelClassName()::withTrashed()->get()[1]->slug);
$this->assertEquals('slug-update', $this->module->getSlugModelClassName()::withTrashed()->get()[2]->slug);
$slugs = $this->module->getSlugModelClassName()::withTrashed()->get();
$this->assertEquals('my-title', $slugs[0]->slug);
$this->assertEquals('slug-update', $slugs[1]->slug);
$this->assertEquals('slug-update', $slugs[2]->slug);

$this->assertCount(3, $this->module->getSlugModelClassName()::withTrashed()->get());
$this->assertCount(3, $slugs);

// Restore the deleted model.
$this->assertTrue($this->module->getRepository()->restore($model->id));

$model = $this->module->getModelClassName()::find($model->id);
$model = $model->fresh();

$this->assertCount(2, $model->slugs()->get());
$this->assertEquals('slug-update-2', $model->getSlug());
}

public function testReactivateSlug(): void
public function testCustomSlugDoesntChangeOnUpdate(): void
{
/** @var Model|HasSlug $model */
$model = $this->module->getRepository()->create([
'title' => 'My title',
'slug' => ['en' => 'my-title'],
'slug' => ['en' => 'my-custom-slug'],
]);

$this->assertEquals('my-title', $model->getSlug());
$this->assertCount(1, $model->slugs()->get());

$model = $this->module->getRepository()->update($model->id, [
'slug' => ['en' => 'slug-update'],
]);
$this->assertEquals('my-custom-slug', $model->getSlug());
$this->assertEquals(1, $model->slugs()->count());

$this->assertEquals('slug-update', $model->getSlug());
$this->assertCount(2, $model->slugs()->get());
$model = $this->module->getRepository()->update($model->id, ['position' => 1]);

$model = $this->module->getRepository()->update($model->id, [
'slug' => ['en' => 'my-title'],
]);
$this->assertEquals(1, $model->slugs()->count());
$this->assertEquals('my-custom-slug', $model->getSlug());

$this->assertEquals('my-title', $model->getSlug());
$this->assertCount(2, $model->slugs()->get());
$model = $this->module->getRepository()->update($model->id, ['title' => 'My new title']);
$this->assertEquals('my-new-title', $model->getSlug());
}
}

0 comments on commit 9521fd6

Please sign in to comment.