Skip to content

Commit 291a807

Browse files
committed
Slugs: Added slug recording at points of generation
Also moved some model-level helpers, which used app container resolution, to be injected services instead.
1 parent e64fc60 commit 291a807

File tree

13 files changed

+141
-64
lines changed

13 files changed

+141
-64
lines changed

app/App/SluggableInterface.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@
55
/**
66
* Assigned to models that can have slugs.
77
* Must have the below properties.
8+
*
9+
* @property string $slug
810
*/
911
interface SluggableInterface
1012
{
11-
/**
12-
* Regenerate the slug for this model.
13-
*/
14-
public function refreshSlug(): string;
1513
}

app/Entities/Models/BookChild.php

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
namespace BookStack\Entities\Models;
44

5-
use BookStack\References\ReferenceUpdater;
65
use Illuminate\Database\Eloquent\Relations\BelongsTo;
76

87
/**
@@ -22,29 +21,4 @@ public function book(): BelongsTo
2221
{
2322
return $this->belongsTo(Book::class)->withTrashed();
2423
}
25-
26-
/**
27-
* Change the book that this entity belongs to.
28-
*/
29-
public function changeBook(int $newBookId): self
30-
{
31-
$oldUrl = $this->getUrl();
32-
$this->book_id = $newBookId;
33-
$this->unsetRelation('book');
34-
$this->refreshSlug();
35-
$this->save();
36-
37-
if ($oldUrl !== $this->getUrl()) {
38-
app()->make(ReferenceUpdater::class)->updateEntityReferences($this, $oldUrl);
39-
}
40-
41-
// Update all child pages if a chapter
42-
if ($this instanceof Chapter) {
43-
foreach ($this->pages()->withTrashed()->get() as $page) {
44-
$page->changeBook($newBookId);
45-
}
46-
}
47-
48-
return $this;
49-
}
5024
}

app/Entities/Models/Entity.php

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
use BookStack\Activity\Models\Watch;
1414
use BookStack\App\Model;
1515
use BookStack\App\SluggableInterface;
16-
use BookStack\Entities\Tools\SlugGenerator;
1716
use BookStack\Permissions\JointPermissionBuilder;
1817
use BookStack\Permissions\Models\EntityPermission;
1918
use BookStack\Permissions\Models\JointPermission;
@@ -405,16 +404,6 @@ public function indexForSearch(): void
405404
app()->make(SearchIndex::class)->indexEntity(clone $this);
406405
}
407406

408-
/**
409-
* {@inheritdoc}
410-
*/
411-
public function refreshSlug(): string
412-
{
413-
$this->slug = app()->make(SlugGenerator::class)->generate($this, $this->name);
414-
415-
return $this->slug;
416-
}
417-
418407
/**
419408
* {@inheritdoc}
420409
*/

app/Entities/Repos/BaseRepo.php

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
use BookStack\Entities\Models\HasDescriptionInterface;
99
use BookStack\Entities\Models\Entity;
1010
use BookStack\Entities\Queries\PageQueries;
11+
use BookStack\Entities\Tools\SlugGenerator;
12+
use BookStack\Entities\Tools\SlugHistory;
1113
use BookStack\Exceptions\ImageUploadException;
1214
use BookStack\References\ReferenceStore;
1315
use BookStack\References\ReferenceUpdater;
@@ -25,6 +27,8 @@ public function __construct(
2527
protected ReferenceStore $referenceStore,
2628
protected PageQueries $pageQueries,
2729
protected BookSorter $bookSorter,
30+
protected SlugGenerator $slugGenerator,
31+
protected SlugHistory $slugHistory,
2832
) {
2933
}
3034

@@ -43,7 +47,7 @@ public function create(Entity $entity, array $input): Entity
4347
'updated_by' => user()->id,
4448
'owned_by' => user()->id,
4549
]);
46-
$entity->refreshSlug();
50+
$this->refreshSlug($entity);
4751

4852
if ($entity instanceof HasDescriptionInterface) {
4953
$this->updateDescription($entity, $input);
@@ -78,7 +82,7 @@ public function update(Entity $entity, array $input): Entity
7882
$entity->updated_by = user()->id;
7983

8084
if ($entity->isDirty('name') || empty($entity->slug)) {
81-
$entity->refreshSlug();
85+
$this->refreshSlug($entity);
8286
}
8387

8488
if ($entity instanceof HasDescriptionInterface) {
@@ -155,4 +159,16 @@ protected function updateDescription(Entity $entity, array $input): void
155159
$entity->descriptionInfo()->set('', $input['description']);
156160
}
157161
}
162+
163+
/**
164+
* Refresh the slug for the given entity.
165+
*/
166+
public function refreshSlug(Entity $entity): void
167+
{
168+
if ($entity->id) {
169+
$this->slugHistory->recordForEntity($entity);
170+
}
171+
172+
$this->slugGenerator->regenerateForEntity($entity);
173+
}
158174
}

app/Entities/Repos/ChapterRepo.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use BookStack\Entities\Models\Chapter;
88
use BookStack\Entities\Queries\EntityQueries;
99
use BookStack\Entities\Tools\BookContents;
10+
use BookStack\Entities\Tools\ParentChanger;
1011
use BookStack\Entities\Tools\TrashCan;
1112
use BookStack\Exceptions\MoveOperationException;
1213
use BookStack\Exceptions\PermissionsException;
@@ -21,6 +22,7 @@ public function __construct(
2122
protected BaseRepo $baseRepo,
2223
protected EntityQueries $entityQueries,
2324
protected TrashCan $trashCan,
25+
protected ParentChanger $parentChanger,
2426
) {
2527
}
2628

@@ -97,7 +99,7 @@ public function move(Chapter $chapter, string $parentIdentifier): Book
9799
}
98100

99101
return (new DatabaseTransaction(function () use ($chapter, $parent) {
100-
$chapter = $chapter->changeBook($parent->id);
102+
$this->parentChanger->changeBook($chapter, $parent->id);
101103
$chapter->rebuildPermissions();
102104
Activity::add(ActivityType::CHAPTER_MOVE, $chapter);
103105

app/Entities/Repos/PageRepo.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use BookStack\Entities\Tools\BookContents;
1313
use BookStack\Entities\Tools\PageContent;
1414
use BookStack\Entities\Tools\PageEditorType;
15+
use BookStack\Entities\Tools\ParentChanger;
1516
use BookStack\Entities\Tools\TrashCan;
1617
use BookStack\Exceptions\MoveOperationException;
1718
use BookStack\Exceptions\PermissionsException;
@@ -31,6 +32,7 @@ public function __construct(
3132
protected ReferenceStore $referenceStore,
3233
protected ReferenceUpdater $referenceUpdater,
3334
protected TrashCan $trashCan,
35+
protected ParentChanger $parentChanger,
3436
) {
3537
}
3638

@@ -242,7 +244,7 @@ public function restoreRevision(Page $page, int $revisionId): Page
242244
}
243245

244246
$page->updated_by = user()->id;
245-
$page->refreshSlug();
247+
$this->baseRepo->refreshSlug($page);
246248
$page->save();
247249
$page->indexForSearch();
248250
$this->referenceStore->updateForEntity($page);
@@ -284,7 +286,7 @@ public function move(Page $page, string $parentIdentifier): Entity
284286
return (new DatabaseTransaction(function () use ($page, $parent) {
285287
$page->chapter_id = ($parent instanceof Chapter) ? $parent->id : null;
286288
$newBookId = ($parent instanceof Chapter) ? $parent->book->id : $parent->id;
287-
$page = $page->changeBook($newBookId);
289+
$this->parentChanger->changeBook($page, $newBookId);
288290
$page->rebuildPermissions();
289291

290292
Activity::add(ActivityType::PAGE_MOVE, $page);

app/Entities/Tools/HierarchyTransformer.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ public function __construct(
1717
protected BookRepo $bookRepo,
1818
protected BookshelfRepo $shelfRepo,
1919
protected Cloner $cloner,
20-
protected TrashCan $trashCan
20+
protected TrashCan $trashCan,
21+
protected ParentChanger $parentChanger,
2122
) {
2223
}
2324

@@ -35,7 +36,7 @@ public function transformChapterToBook(Chapter $chapter): Book
3536
foreach ($chapter->pages as $page) {
3637
$page->chapter_id = 0;
3738
$page->save();
38-
$page->changeBook($book->id);
39+
$this->parentChanger->changeBook($page, $book->id);
3940
}
4041

4142
$this->trashCan->destroyEntity($chapter);
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
namespace BookStack\Entities\Tools;
4+
5+
use BookStack\Entities\Models\BookChild;
6+
use BookStack\Entities\Models\Chapter;
7+
use BookStack\References\ReferenceUpdater;
8+
9+
class ParentChanger
10+
{
11+
public function __construct(
12+
protected SlugGenerator $slugGenerator,
13+
protected ReferenceUpdater $referenceUpdater
14+
) {
15+
}
16+
17+
/**
18+
* Change the parent book of a chapter or page.
19+
*/
20+
public function changeBook(BookChild $child, int $newBookId): void
21+
{
22+
$oldUrl = $child->getUrl();
23+
24+
$child->book_id = $newBookId;
25+
$child->unsetRelation('book');
26+
$this->slugGenerator->regenerateForEntity($child);
27+
$child->save();
28+
29+
if ($oldUrl !== $child->getUrl()) {
30+
$this->referenceUpdater->updateEntityReferences($child, $oldUrl);
31+
}
32+
33+
// Update all child pages if a chapter
34+
if ($child instanceof Chapter) {
35+
foreach ($child->pages()->withTrashed()->get() as $page) {
36+
$this->changeBook($page, $newBookId);
37+
}
38+
}
39+
}
40+
}

app/Entities/Tools/SlugGenerator.php

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
use BookStack\App\Model;
66
use BookStack\App\SluggableInterface;
77
use BookStack\Entities\Models\BookChild;
8+
use BookStack\Entities\Models\Entity;
9+
use BookStack\Users\Models\User;
810
use Illuminate\Support\Str;
911

1012
class SlugGenerator
1113
{
1214
/**
13-
* Generate a fresh slug for the given entity.
15+
* Generate a fresh slug for the given item.
1416
* The slug will be generated so that it doesn't conflict within the same parent item.
1517
*/
1618
public function generate(SluggableInterface&Model $model, string $slugSource): string
@@ -23,6 +25,26 @@ public function generate(SluggableInterface&Model $model, string $slugSource): s
2325
return $slug;
2426
}
2527

28+
/**
29+
* Regenerate the slug for the given entity.
30+
*/
31+
public function regenerateForEntity(Entity $entity): string
32+
{
33+
$entity->slug = $this->generate($entity, $entity->name);
34+
35+
return $entity->slug;
36+
}
37+
38+
/**
39+
* Regenerate the slug for a user.
40+
*/
41+
public function regenerateForUser(User $user): string
42+
{
43+
$user->slug = $this->generate($user, $user->name);
44+
45+
return $user->slug;
46+
}
47+
2648
/**
2749
* Format a name as a URL slug.
2850
*/

app/Entities/Tools/SlugHistory.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
namespace BookStack\Entities\Tools;
4+
5+
use BookStack\Entities\Models\Entity;
6+
use Illuminate\Support\Facades\DB;
7+
8+
class SlugHistory
9+
{
10+
/**
11+
* Record the current slugs for the given entity.
12+
*/
13+
public function recordForEntity(Entity $entity): void
14+
{
15+
$latest = $this->getLatestEntryForEntity($entity);
16+
if ($latest && $latest->slug === $entity->slug && $latest->parent_slug === $entity->getParent()?->slug) {
17+
return;
18+
}
19+
20+
$info = [
21+
'sluggable_type' => $entity->getMorphClass(),
22+
'sluggable_id' => $entity->id,
23+
'slug' => $entity->slug,
24+
'parent_slug' => $entity->getParent()?->slug,
25+
'created_at' => now(),
26+
'updated_at' => now(),
27+
];
28+
29+
DB::table('slug_history')->insert($info);
30+
}
31+
32+
protected function getLatestEntryForEntity(Entity $entity): \stdClass|null
33+
{
34+
return DB::table('slug_history')
35+
->where('sluggable_type', '=', $entity->getMorphClass())
36+
->where('sluggable_id', '=', $entity->id)
37+
->orderBy('created_at', 'desc')
38+
->first();
39+
}
40+
}

0 commit comments

Comments
 (0)