For book, shelves and chapters.
Made much of the existing handling generic to entity types.
Added new MixedEntityListLoader to help load lists somewhat efficiently.
Only manually tested so far.
DB::setDefaultConnection($this->option('database'));
}
- $references->updateForAllPages();
+ $references->updateForAll();
DB::setDefaultConnection($connection);
'bookParentShelves' => $bookParentShelves,
'watchOptions' => new UserEntityWatchOptions(user(), $book),
'activity' => $activities->entityActivity($book, 20, 1),
- 'referenceCount' => $this->referenceFetcher->getPageReferenceCountToEntity($book),
+ 'referenceCount' => $this->referenceFetcher->getReferenceCountToEntity($book),
]);
}
'view' => $view,
'activity' => $activities->entityActivity($shelf, 20, 1),
'listOptions' => $listOptions,
- 'referenceCount' => $this->referenceFetcher->getPageReferenceCountToEntity($shelf),
+ 'referenceCount' => $this->referenceFetcher->getReferenceCountToEntity($shelf),
]);
}
'pages' => $pages,
'next' => $nextPreviousLocator->getNext(),
'previous' => $nextPreviousLocator->getPrevious(),
- 'referenceCount' => $this->referenceFetcher->getPageReferenceCountToEntity($chapter),
+ 'referenceCount' => $this->referenceFetcher->getReferenceCountToEntity($chapter),
]);
}
'watchOptions' => new UserEntityWatchOptions(user(), $page),
'next' => $nextPreviousLocator->getNext(),
'previous' => $nextPreviousLocator->getPrevious(),
- 'referenceCount' => $this->referenceFetcher->getPageReferenceCountToEntity($page),
+ 'referenceCount' => $this->referenceFetcher->getReferenceCountToEntity($page),
]);
}
use HasFactory;
use HasHtmlDescription;
- public $searchFactor = 1.2;
+ public float $searchFactor = 1.2;
protected $fillable = ['name'];
protected $hidden = ['pivot', 'image_id', 'deleted_at'];
protected $table = 'bookshelves';
- public $searchFactor = 1.2;
+ public float $searchFactor = 1.2;
protected $fillable = ['name', 'description', 'image_id'];
use HasFactory;
use HasHtmlDescription;
- public $searchFactor = 1.2;
+ public float $searchFactor = 1.2;
protected $fillable = ['name', 'description', 'priority'];
protected $hidden = ['pivot', 'deleted_at'];
/**
* @var string - Name of property where the main text content is found
*/
- public $textField = 'description';
+ public string $textField = 'description';
+
+ /**
+ * @var string - Name of the property where the main HTML content is found
+ */
+ public string $htmlField = 'description_html';
/**
* @var float - Multiplier for search indexing.
*/
- public $searchFactor = 1.0;
+ public float $searchFactor = 1.0;
/**
* Get the entities that are visible to the current user.
protected $fillable = ['name', 'priority'];
- public $textField = 'text';
+ public string $textField = 'text';
+ public string $htmlField = 'html';
protected $hidden = ['html', 'markdown', 'text', 'pivot', 'deleted_at'];
use BookStack\Entities\Models\HasCoverImage;
use BookStack\Entities\Models\HasHtmlDescription;
use BookStack\Exceptions\ImageUploadException;
+use BookStack\References\ReferenceStore;
use BookStack\References\ReferenceUpdater;
use BookStack\Uploads\ImageRepo;
use Illuminate\Http\UploadedFile;
public function __construct(
protected TagRepo $tagRepo,
protected ImageRepo $imageRepo,
- protected ReferenceUpdater $referenceUpdater
+ protected ReferenceUpdater $referenceUpdater,
+ protected ReferenceStore $referenceStore,
) {
}
$entity->refresh();
$entity->rebuildPermissions();
$entity->indexForSearch();
+ $this->referenceStore->updateForEntity($entity);
}
/**
$entity->rebuildPermissions();
$entity->indexForSearch();
+ $this->referenceStore->updateForEntity($entity);
if ($oldUrl !== $entity->getUrl()) {
$this->referenceUpdater->updateEntityPageReferences($entity, $oldUrl);
$this->baseRepo->update($draft, $input);
$this->revisionRepo->storeNewForPage($draft, trans('entities.pages_initial_revision'));
- $this->referenceStore->updateForPage($draft);
$draft->refresh();
Activity::add(ActivityType::PAGE_CREATE, $draft);
$this->updateTemplateStatusAndContentFromInput($page, $input);
$this->baseRepo->update($page, $input);
- $this->referenceStore->updateForPage($page);
// Update with new details
$page->revision_count++;
$page->refreshSlug();
$page->save();
$page->indexForSearch();
- $this->referenceStore->updateForPage($page);
+ $this->referenceStore->updateForEntity($page);
$summary = trans('entities.pages_revision_restored_from', ['id' => strval($revisionId), 'summary' => $revision->summary]);
$this->revisionRepo->storeNewForPage($page, $summary);
--- /dev/null
+<?php
+
+namespace BookStack\Entities\Tools;
+
+use BookStack\App\Model;
+use BookStack\Entities\EntityProvider;
+use Illuminate\Database\Eloquent\Relations\Relation;
+
+class MixedEntityListLoader
+{
+ protected array $listAttributes = [
+ 'page' => ['id', 'name', 'slug', 'book_id', 'chapter_id', 'text', 'draft'],
+ 'chapter' => ['id', 'name', 'slug', 'book_id', 'description'],
+ 'book' => ['id', 'name', 'slug', 'description'],
+ 'bookshelf' => ['id', 'name', 'slug', 'description'],
+ ];
+
+ public function __construct(
+ protected EntityProvider $entityProvider
+ ) {
+ }
+
+ /**
+ * Efficiently load in entities for listing onto the given list
+ * where entities are set as a relation via the given name.
+ * This will look for a model id and type via 'name_id' and 'name_type'.
+ * @param Model[] $relations
+ */
+ public function loadIntoRelations(array $relations, string $relationName): void
+ {
+ $idsByType = [];
+ foreach ($relations as $relation) {
+ $type = $relation->getAttribute($relationName . '_type');
+ $id = $relation->getAttribute($relationName . '_id');
+
+ if (!isset($idsByType[$type])) {
+ $idsByType[$type] = [];
+ }
+
+ $idsByType[$type][] = $id;
+ }
+
+ $modelMap = $this->idsByTypeToModelMap($idsByType);
+
+ foreach ($relations as $relation) {
+ $type = $relation->getAttribute($relationName . '_type');
+ $id = $relation->getAttribute($relationName . '_id');
+ $related = $modelMap[$type][strval($id)] ?? null;
+ if ($related) {
+ $relation->setRelation($relationName, $related);
+ }
+ }
+ }
+
+ /**
+ * @param array<string, int[]> $idsByType
+ * @return array<string, array<int, Model>>
+ */
+ protected function idsByTypeToModelMap(array $idsByType): array
+ {
+ $modelMap = [];
+
+ foreach ($idsByType as $type => $ids) {
+ if (!isset($this->listAttributes[$type])) {
+ continue;
+ }
+
+ $instance = $this->entityProvider->get($type);
+ $models = $instance->newQuery()
+ ->select($this->listAttributes[$type])
+ ->scopes('visible')
+ ->whereIn('id', $ids)
+ ->with($this->getRelationsToEagerLoad($type))
+ ->get();
+
+ if (count($models) > 0) {
+ $modelMap[$type] = [];
+ }
+
+ foreach ($models as $model) {
+ $modelMap[$type][strval($model->id)] = $model;
+ }
+ }
+
+ return $modelMap;
+ }
+
+ protected function getRelationsToEagerLoad(string $type): array
+ {
+ $toLoad = [];
+ $loadVisible = fn (Relation $query) => $query->scopes('visible');
+
+ if ($type === 'chapter' || $type === 'page') {
+ $toLoad['book'] = $loadVisible;
+ }
+
+ if ($type === 'page') {
+ $toLoad['chapter'] = $loadVisible;
+ }
+
+ return $toLoad;
+ }
+}
class ReferenceController extends Controller
{
- protected ReferenceFetcher $referenceFetcher;
-
- public function __construct(ReferenceFetcher $referenceFetcher)
- {
- $this->referenceFetcher = $referenceFetcher;
+ public function __construct(
+ protected ReferenceFetcher $referenceFetcher
+ ) {
}
/**
public function page(string $bookSlug, string $pageSlug)
{
$page = Page::getBySlugs($bookSlug, $pageSlug);
- $references = $this->referenceFetcher->getPageReferencesToEntity($page);
+ $references = $this->referenceFetcher->getReferencesToEntity($page);
return view('pages.references', [
'page' => $page,
public function chapter(string $bookSlug, string $chapterSlug)
{
$chapter = Chapter::getBySlugs($bookSlug, $chapterSlug);
- $references = $this->referenceFetcher->getPageReferencesToEntity($chapter);
+ $references = $this->referenceFetcher->getReferencesToEntity($chapter);
return view('chapters.references', [
'chapter' => $chapter,
public function book(string $slug)
{
$book = Book::getBySlug($slug);
- $references = $this->referenceFetcher->getPageReferencesToEntity($book);
+ $references = $this->referenceFetcher->getReferencesToEntity($book);
return view('books.references', [
'book' => $book,
public function shelf(string $slug)
{
$shelf = Bookshelf::getBySlug($slug);
- $references = $this->referenceFetcher->getPageReferencesToEntity($shelf);
+ $references = $this->referenceFetcher->getReferencesToEntity($shelf);
return view('shelves.references', [
'shelf' => $shelf,
namespace BookStack\References;
use BookStack\Entities\Models\Entity;
-use BookStack\Entities\Models\Page;
+use BookStack\Entities\Tools\MixedEntityListLoader;
use BookStack\Permissions\PermissionApplicator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
-use Illuminate\Database\Eloquent\Relations\Relation;
class ReferenceFetcher
{
- protected PermissionApplicator $permissions;
-
- public function __construct(PermissionApplicator $permissions)
- {
- $this->permissions = $permissions;
+ public function __construct(
+ protected PermissionApplicator $permissions,
+ protected MixedEntityListLoader $mixedEntityListLoader,
+ ) {
}
/**
- * Query and return the page references pointing to the given entity.
+ * Query and return the references pointing to the given entity.
* Loads the commonly required relations while taking permissions into account.
*/
- public function getPageReferencesToEntity(Entity $entity): Collection
+ public function getReferencesToEntity(Entity $entity): Collection
{
- $baseQuery = $this->queryPageReferencesToEntity($entity)
- ->with([
- 'from' => fn (Relation $query) => $query->select(Page::$listAttributes),
- 'from.book' => fn (Relation $query) => $query->scopes('visible'),
- 'from.chapter' => fn (Relation $query) => $query->scopes('visible'),
- ]);
-
- $references = $this->permissions->restrictEntityRelationQuery(
- $baseQuery,
- 'references',
- 'from_id',
- 'from_type'
- )->get();
+ $references = $this->queryReferencesToEntity($entity)->get();
+ $this->mixedEntityListLoader->loadIntoRelations($references->all(), 'from');
return $references;
}
/**
- * Returns the count of page references pointing to the given entity.
+ * Returns the count of references pointing to the given entity.
* Takes permissions into account.
*/
- public function getPageReferenceCountToEntity(Entity $entity): int
+ public function getReferenceCountToEntity(Entity $entity): int
{
- $count = $this->permissions->restrictEntityRelationQuery(
- $this->queryPageReferencesToEntity($entity),
- 'references',
- 'from_id',
- 'from_type'
- )->count();
-
- return $count;
+ return $this->queryReferencesToEntity($entity)->count();
}
- protected function queryPageReferencesToEntity(Entity $entity): Builder
+ protected function queryReferencesToEntity(Entity $entity): Builder
{
- return Reference::query()
+ $baseQuery = Reference::query()
->where('to_type', '=', $entity->getMorphClass())
- ->where('to_id', '=', $entity->id)
- ->where('from_type', '=', (new Page())->getMorphClass());
+ ->where('to_id', '=', $entity->id);
+
+ return $this->permissions->restrictEntityRelationQuery(
+ $baseQuery,
+ 'references',
+ 'from_id',
+ 'from_type'
+ );
}
}
namespace BookStack\References;
-use BookStack\Entities\Models\Page;
+use BookStack\Entities\EntityProvider;
+use BookStack\Entities\Models\Entity;
use Illuminate\Database\Eloquent\Collection;
class ReferenceStore
{
+ public function __construct(
+ protected EntityProvider $entityProvider
+ ) {
+ }
+
/**
- * Update the outgoing references for the given page.
+ * Update the outgoing references for the given entity.
*/
- public function updateForPage(Page $page): void
+ public function updateForEntity(Entity $entity): void
{
- $this->updateForPages([$page]);
+ $this->updateForEntities([$entity]);
}
/**
- * Update the outgoing references for all pages in the system.
+ * Update the outgoing references for all entities in the system.
*/
- public function updateForAllPages(): void
+ public function updateForAll(): void
{
- Reference::query()
- ->where('from_type', '=', (new Page())->getMorphClass())
- ->delete();
+ Reference::query()->delete();
- Page::query()->select(['id', 'html'])->chunk(100, function (Collection $pages) {
- $this->updateForPages($pages->all());
- });
+ foreach ($this->entityProvider->all() as $entity) {
+ $entity->newQuery()->select(['id', $entity->htmlField])->chunk(100, function (Collection $entities) {
+ $this->updateForEntities($entities->all());
+ });
+ }
}
/**
- * Update the outgoing references for the pages in the given array.
+ * Update the outgoing references for the entities in the given array.
*
- * @param Page[] $pages
+ * @param Entity[] $entities
*/
- protected function updateForPages(array $pages): void
+ protected function updateForEntities(array $entities): void
{
- if (count($pages) === 0) {
+ if (count($entities) === 0) {
return;
}
$parser = CrossLinkParser::createWithEntityResolvers();
$references = [];
- $pageIds = array_map(fn (Page $page) => $page->id, $pages);
- Reference::query()
- ->where('from_type', '=', $pages[0]->getMorphClass())
- ->whereIn('from_id', $pageIds)
- ->delete();
+ $this->dropReferencesFromEntities($entities);
- foreach ($pages as $page) {
- $models = $parser->extractLinkedModels($page->html);
+ foreach ($entities as $entity) {
+ $models = $parser->extractLinkedModels($entity->getAttribute($entity->htmlField));
foreach ($models as $model) {
$references[] = [
- 'from_id' => $page->id,
- 'from_type' => $page->getMorphClass(),
+ 'from_id' => $entity->id,
+ 'from_type' => $entity->getMorphClass(),
'to_id' => $model->id,
'to_type' => $model->getMorphClass(),
];
Reference::query()->insert($referenceDataChunk);
}
}
+
+ /**
+ * Delete all the existing references originating from the given entities.
+ * @param Entity[] $entities
+ */
+ protected function dropReferencesFromEntities(array $entities): void
+ {
+ $IdsByType = [];
+
+ foreach ($entities as $entity) {
+ $type = $entity->getMorphClass();
+ if (!isset($IdsByType[$type])) {
+ $IdsByType[$type] = [];
+ }
+
+ $IdsByType[$type][] = $entity->id;
+ }
+
+ foreach ($IdsByType as $type => $entityIds) {
+ Reference::query()
+ ->where('from_type', '=', $type)
+ ->whereIn('from_id', $entityIds)
+ ->delete();
+ }
+ }
}
protected function getReferencesToUpdate(Entity $entity): array
{
/** @var Reference[] $references */
- $references = $this->referenceFetcher->getPageReferencesToEntity($entity)->values()->all();
+ $references = $this->referenceFetcher->getReferencesToEntity($entity)->values()->all();
if ($entity instanceof Book) {
$pages = $entity->pages()->get(['id']);
$children = $pages->concat($chapters);
foreach ($children as $bookChild) {
/** @var Reference[] $childRefs */
- $childRefs = $this->referenceFetcher->getPageReferencesToEntity($bookChild)->values()->all();
+ $childRefs = $this->referenceFetcher->getReferencesToEntity($bookChild)->values()->all();
array_push($references, ...$childRefs);
}
}
$this->logActivity(ActivityType::MAINTENANCE_ACTION_RUN, 'regenerate-references');
try {
- $referenceStore->updateForAllPages();
+ $referenceStore->updateForAll();
$this->showSuccessNotification(trans('settings.maint_regen_references_success'));
} catch (\Exception $exception) {
$this->showErrorNotification($exception->getMessage());
'meta_updated' => 'Updated :timeLength',
'meta_updated_name' => 'Updated :timeLength by :user',
'meta_owned_name' => 'Owned by :user',
- 'meta_reference_page_count' => 'Referenced on :count page|Referenced on :count pages',
+ 'meta_reference_count' => 'Referenced by :count item|Referenced by :count items',
'entity_select' => 'Entity Select',
'entity_select_lack_permission' => 'You don\'t have the required permissions to select this item',
'images' => 'Images',
// References
'references' => 'References',
'references_none' => 'There are no tracked references to this item.',
- 'references_to_desc' => 'Shown below are all the known pages in the system that link to this item.',
+ 'references_to_desc' => 'Listed below is all the known content in the system that links to this item.',
// Watch Options
'watch' => 'Watch',
<a href="{{ $entity->getUrl('/references') }}" class="entity-meta-item">
@icon('reference')
<div>
- {!! trans_choice('entities.meta_reference_page_count', $referenceCount, ['count' => $referenceCount]) !!}
+ {{ trans_choice('entities.meta_reference_count', $referenceCount, ['count' => $referenceCount]) }}
</div>
</a>
@endif
</div>
<div class="book-content">
- <p class="text-muted">{!! nl2br(e($shelf->description)) !!}</p>
+ <p class="text-muted">{!! $shelf->descriptionHtml() !!}</p>
@if(count($sortedVisibleShelfBooks) > 0)
@if($view === 'list')
<div class="entity-list">