Dan Brown

Added indexes, Reduced queries on pages

......@@ -12,3 +12,4 @@ Homestead.yaml
/public/build
/storage/images
_ide_helper.php
/storage/debugbar
\ No newline at end of file
......
......@@ -27,16 +27,6 @@ class Book extends Entity
return $this->hasMany('BookStack\Chapter');
}
public function children()
{
$pages = $this->pages()->where('chapter_id', '=', 0)->get();
$chapters = $this->chapters()->get();
foreach($chapters as $chapter) {
$pages->push($chapter);
}
return $pages->sortBy('priority');
}
public function getExcerpt($length = 100)
{
return strlen($this->description) > $length ? substr($this->description, 0, $length-3) . '...' : $this->description;
......
......@@ -18,7 +18,8 @@ class Chapter extends Entity
public function getUrl()
{
return '/books/' . $this->book->slug . '/chapter/' . $this->slug;
$bookSlug = isset($this->bookSlug) ? $this->bookSlug : $this->book->slug;
return '/books/' . $bookSlug. '/chapter/' . $this->slug;
}
public function getExcerpt($length = 100)
......
......@@ -89,7 +89,8 @@ class BookController extends Controller
{
$book = $this->bookRepo->getBySlug($slug);
Views::add($book);
return view('books/show', ['book' => $book, 'current' => $book]);
$bookChildren = $this->bookRepo->getChildren($book);
return view('books/show', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]);
}
/**
......
......@@ -80,8 +80,9 @@ class ChapterController extends Controller
{
$book = $this->bookRepo->getBySlug($bookSlug);
$chapter = $this->chapterRepo->getBySlug($chapterSlug, $book->id);
$sidebarTree = $this->bookRepo->getChildren($book);
Views::add($chapter);
return view('chapters/show', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter]);
return view('chapters/show', ['book' => $book, 'chapter' => $chapter, 'current' => $chapter, 'sidebarTree' => $sidebarTree]);
}
/**
......
......@@ -87,8 +87,9 @@ class PageController extends Controller
{
$book = $this->bookRepo->getBySlug($bookSlug);
$page = $this->pageRepo->getBySlug($pageSlug, $book->id);
$sidebarTree = $this->bookRepo->getChildren($book);
Views::add($page);
return view('pages/show', ['page' => $page, 'book' => $book, 'current' => $page]);
return view('pages/show', ['page' => $page, 'book' => $book, 'current' => $page, 'sidebarTree' => $sidebarTree]);
}
/**
......
......@@ -40,7 +40,9 @@ class Page extends Entity
public function getUrl()
{
return '/books/' . $this->book->slug . '/page/' . $this->slug;
// TODO - Extract this and share with chapters
$bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
return '/books/' . $bookSlug . '/page/' . $this->slug;
}
public function getExcerpt($length = 100)
......
......@@ -179,6 +179,30 @@ class BookRepo
}
/**
* Get all child objects of a book.
* Returns a sorted collection of Pages and Chapters.
* Loads the bookslug onto child elements to prevent access database access for getting the slug.
* @param Book $book
* @return mixed
*/
public function getChildren(Book $book)
{
$pages = $book->pages()->where('chapter_id', '=', 0)->get();
$chapters = $book->chapters()->with('pages')->get();
$children = $pages->merge($chapters);
$bookSlug = $book->slug;
$children->each(function ($child) use ($bookSlug) {
$child->setAttribute('bookSlug', $bookSlug);
if ($child->isA('chapter')) {
$child->pages->each(function ($page) use ($bookSlug) {
$page->setAttribute('bookSlug', $bookSlug);
});
}
});
return $children->sortBy('priority');
}
/**
* Get books by search term.
* @param $term
* @return mixed
......
......@@ -34,6 +34,12 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
protected $hidden = ['password', 'remember_token'];
/**
* This holds the user's permissions when loaded.
* @var array
*/
protected $permissions;
/**
* Returns a default guest user.
*/
public static function getDefault()
......@@ -58,7 +64,19 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
public function getRoleAttribute()
{
return $this->roles()->first();
return $this->roles()->with('permissions')->first();
}
/**
* Loads the user's permissions from thier role.
*/
private function loadPermissions()
{
if (isset($this->permissions)) return;
$this->load('roles.permissions');
$permissions = $this->roles[0]->permissions;
$permissionsArray = $permissions->pluck('name')->all();
$this->permissions = $permissionsArray;
}
/**
......@@ -68,14 +86,11 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
public function can($permissionName)
{
if($this->email == 'guest') {
if ($this->email == 'guest') {
return false;
}
$permissions = $this->role->permissions()->get();
$permissionSearch = $permissions->search(function ($item, $key) use ($permissionName) {
return $item->name == $permissionName;
});
return $permissionSearch !== false;
$this->loadPermissions();
return array_search($permissionName, $this->permissions) !== false;
}
/**
......@@ -114,7 +129,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
public function hasSocialAccount($socialDriver = false)
{
if($socialDriver === false) {
if ($socialDriver === false) {
return $this->socialAccounts()->count() > 0;
}
......
......@@ -9,7 +9,8 @@
"laravel/framework": "5.1.*",
"intervention/image": "^2.3",
"barryvdh/laravel-ide-helper": "^2.1",
"laravel/socialite": "^2.0"
"laravel/socialite": "^2.0",
"barryvdh/laravel-debugbar": "^2.0"
},
"require-dev": {
"fzaninotto/faker": "~1.4",
......
......@@ -143,6 +143,7 @@ return [
*/
Intervention\Image\ImageServiceProvider::class,
Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class,
Barryvdh\Debugbar\ServiceProvider::class,
/*
......@@ -207,6 +208,7 @@ return [
*/
'ImageTool' => Intervention\Image\Facades\Image::class,
'Debugbar' => Barryvdh\Debugbar\Facade::class,
/**
* Custom
......
......@@ -23,6 +23,7 @@ $factory->define(BookStack\User::class, function ($faker) {
$factory->define(BookStack\Book::class, function ($faker) {
return [
'name' => $faker->sentence,
'slug' => str_random(10),
'description' => $faker->paragraph
];
});
......@@ -30,6 +31,7 @@ $factory->define(BookStack\Book::class, function ($faker) {
$factory->define(BookStack\Chapter::class, function ($faker) {
return [
'name' => $faker->sentence,
'slug' => str_random(10),
'description' => $faker->paragraph
];
});
......@@ -37,6 +39,7 @@ $factory->define(BookStack\Chapter::class, function ($faker) {
$factory->define(BookStack\Page::class, function ($faker) {
return [
'name' => $faker->sentence,
'slug' => str_random(10),
'html' => '<p>' . implode('</p>', $faker->paragraphs(5)) . '</p>'
];
});
......
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddEntityIndexes extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('books', function (Blueprint $table) {
$table->index('slug');
$table->index('created_by');
$table->index('updated_by');
});
Schema::table('pages', function (Blueprint $table) {
$table->index('slug');
$table->index('book_id');
$table->index('chapter_id');
$table->index('priority');
$table->index('created_by');
$table->index('updated_by');
});
Schema::table('page_revisions', function (Blueprint $table) {
$table->index('page_id');
});
Schema::table('chapters', function (Blueprint $table) {
$table->index('slug');
$table->index('book_id');
$table->index('priority');
$table->index('created_by');
$table->index('updated_by');
});
Schema::table('activities', function (Blueprint $table) {
$table->index('book_id');
$table->index('user_id');
$table->index('entity_id');
});
Schema::table('views', function (Blueprint $table) {
$table->index('user_id');
$table->index('viewable_id');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('books', function (Blueprint $table) {
$table->dropIndex('slug');
$table->dropIndex('created_by');
$table->dropIndex('updated_by');
});
Schema::table('pages', function (Blueprint $table) {
$table->dropIndex('slug');
$table->dropIndex('book_id');
$table->dropIndex('chapter_id');
$table->dropIndex('priority');
$table->dropIndex('created_by');
$table->dropIndex('updated_by');
});
Schema::table('page_revisions', function (Blueprint $table) {
$table->dropIndex('page_id');
});
Schema::table('chapters', function (Blueprint $table) {
$table->dropIndex('slug');
$table->dropIndex('book_id');
$table->dropIndex('priority');
$table->dropIndex('created_by');
$table->dropIndex('updated_by');
});
Schema::table('activities', function (Blueprint $table) {
$table->dropIndex('book_id');
$table->dropIndex('user_id');
$table->dropIndex('entity_id');
});
Schema::table('views', function (Blueprint $table) {
$table->dropIndex('user_id');
$table->dropIndex('entity_id');
});
}
}
<?php
use Illuminate\Database\Seeder;
class DummyContentSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
$user = factory(BookStack\User::class, 1)->create();
$role = \BookStack\Role::where('name', '=', 'admin')->first();
$user->attachRole($role);
$books = factory(BookStack\Book::class, 20)->create(['created_by' => $user->id, 'updated_by' => $user->id])
->each(function($book) use ($user) {
$chapters = factory(BookStack\Chapter::class, 5)->create(['created_by' => $user->id, 'updated_by' => $user->id])
->each(function($chapter) use ($user, $book){
$pages = factory(\BookStack\Page::class, 10)->make(['created_by' => $user->id, 'updated_by' => $user->id, 'book_id' => $book->id]);
$chapter->pages()->saveMany($pages);
});
$book->chapters()->saveMany($chapters);
});
}
}
......@@ -37,8 +37,8 @@
<div class="page-list">
<hr>
@if(count($book->children()) > 0)
@foreach($book->children() as $childElement)
@if(count($bookChildren) > 0)
@foreach($bookChildren as $childElement)
@if($childElement->isA('chapter'))
@include('chapters/list-item', ['chapter' => $childElement])
@else
......
......@@ -60,7 +60,7 @@
</p>
</div>
<div class="col-md-3 col-md-offset-1">
@include('pages/sidebar-tree-list', ['book' => $book])
@include('pages/sidebar-tree-list', ['book' => $book, 'sidebarTree' => $sidebarTree])
</div>
</div>
</div>
......
......@@ -60,7 +60,7 @@
</div>
<div class="col-md-3 print-hidden">
@include('pages/sidebar-tree-list', ['book' => $book])
@include('pages/sidebar-tree-list', ['book' => $book, 'sidebarTree' => $sidebarTree])
</div>
</div>
......
......@@ -3,7 +3,7 @@
<h6 class="text-muted">Book Navigation</h6>
<ul class="sidebar-page-list menu">
<li class="book-header"><a href="{{$book->getUrl()}}" class="book {{ $current->matches($book)? 'selected' : '' }}"><i class="zmdi zmdi-book"></i>{{$book->name}}</a></li>
@foreach($book->children() as $bookChild)
@foreach($sidebarTree as $bookChild)
<li class="list-item-{{ $bookChild->getName() }}">
<a href="{{$bookChild->getUrl()}}" class="{{ $bookChild->getName() }} {{ $current->matches($bookChild)? 'selected' : '' }}">
@if($bookChild->isA('chapter'))<i class="zmdi zmdi-collection-bookmark"></i>@else <i class="zmdi zmdi-file-text"></i>@endif{{ $bookChild->name }}
......