I have made a Laravel 8 blogging application that supports themes.
I had made a custom 404 template for every theme, the path to it being 'resources\views\themes\' . $theme_directory . '\templates\404.blade.php', where $theme_directory is the name of the directory of the theme.
The active theme is set in the settings table, and the parent controller reads it from there:
class FrontendController extends Controller
{
protected $data;
protected $site_settings;
protected $theme_directory;
protected $site_name;
protected $tagline;
protected $owner_name;
protected $owner_email;
protected $twitter;
protected $facebook;
protected $instagram;
protected $is_cookieconsent;
protected $is_infinitescroll;
protected $pages;
protected $articles;
protected $article_categories;
protected $article_tags;
protected $authors;
public function __construct()
{
$this->site_settings = Settings::first();
$this->theme_directory = $this->site_settings['theme_directory'] ?? null;
$this->site_name = $this->site_settings['site_name'] ?? null;
$this->tagline = $this->site_settings['tagline'] ?? null;
$this->owner_name = $this->site_settings['owner_name'] ?? null;
$this->owner_email = $this->site_settings['owner_email'] ?? null;
$this->twitter = $this->site_settings['twitter'] ?? null;
$this->facebook = $this->site_settings['facebook'] ?? null;
$this->instagram = $this->site_settings['instagram'] ?? null;
$this->is_cookieconsent = $this->site_settings['is_cookieconsent'] ?? null;
$this->is_infinitescroll = $this->site_settings['is_infinitescroll'] ?? null;
// Most recent articles
$this->articles = Article::visible()->limit(5)->get();
// Article categories. Get only categories with articles
$this->article_categories = ArticleCategory::has('articles')->get();
// Pages
$this->pages = Page::all();
// Article tags
$this->article_tags = Tag::all();
// Authors
$this->authors = User::withCount('articles')
->having('articles_count', '>', 0)
->orderByDesc('articles_count')
->get();
$this->data = [
'theme_directory' => $this->theme_directory,
'site_name' => $this->site_name,
'tagline' => $this->tagline,
'owner_name' => $this->owner_name,
'owner_email' => $this->owner_email,
'twitter' => $this->twitter,
'facebook' => $this->facebook,
'instagram' => $this->instagram,
'is_cookieconsent' => $this->is_cookieconsent,
'is_infinitescroll' => $this->is_infinitescroll,
'pages' => $this->pages,
'articles' => $this->articles,
'categories' => $this->article_categories,
'tags' => $this->article_tags,
'authors' => $this->authors,
];
}
}
But Laravel displayed its default 404 view.
I solved this by adding a render() method in app\Exceptions\Handler.php, wich made look like so:
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use App\Models\Settings;
use App\Models\Page;
use App\Models\ArticleCategory;
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that are not reported.
*
* @var array<int, class-string<Throwable>>
*/
protected $dontReport = [
//
];
/**
* A list of the inputs that are never flashed for validation exceptions.
*
* @var array<int, string>
*/
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
/**
* Register the exception handling callbacks for the application.
*
* @return void
*/
public function register()
{
$this->reportable(function (Throwable $e) {
//
});
}
public function render($request, Throwable $exception)
{
if ($exception instanceof ModelNotFoundException || $exception instanceof NotFoundHttpException) {
// Get the active theme
$settings = Settings::first();
// Pages and categories
$pages = Page::all();
$article_categories = ArticleCategory::has('articles')->get();
// Check if that view exists
if (view()->exists('themes/' . $settings->theme_directory . '/templates/404')) {
$theme_404_view = 'themes/' . $settings->theme_directory . '/templates/404';
return response()->view($theme_404_view, [
'theme_directory' => $settings->theme_directory,
'site_name' => $settings->site_name,
'tagline' => $settings->tagline,
'owner_name' => $settings->owner_name,
'owner_email' => $settings->owner_email,
'twitter' => $settings->twitter,
'facebook' => $settings->facebook,
'instagram' => $settings->instagram,
'is_cookieconsent' => $settings->is_cookieconsent,
'is_infinitescroll' => $settings->is_infinitescroll,
'title' => "404",
'subtitle' => "Article not found",
'message'=> "Nothing to see here!",
'pages' => $pages,
'categories' => $article_categories
], 404);
}
}
return parent::render($request, $exception);
}
}
In app\Http\Controllers\ArticlesController.php I have:
class ArticlesController extends FrontendController
{
protected $per_page = 12;
protected $comments_per_page = 10;
protected $comments_orderby_direction = 'desc';
public function index(Request $request)
{
if (Settings::count() == 0) {
return redirect()->route('dashboard');
}
$qry = $request->input('search');
$articlesQuery = Article::visible();
if (!empty($qry)) {
$articlesQuery->where(function ($q) use ($qry) {
$q->where('title', 'like', '%' . $qry . '%')
->orWhere('short_description', 'like', '%' . $qry . '%')
->orWhere('content', 'like', '%' . $qry . '%');
});
$article_count = $articlesQuery->count();
}
$articles = $articlesQuery->paginate($this->per_page);
$featured_articles = Article::visible()
->where('featured', 1)
->get();
return view(
'themes/' . $this->theme_directory . '/templates/index',
array_merge($this->data, [
'search_query' => $qry,
'articles' => $articles,
'featured_articles' => $featured_articles,
'article_count' => $article_count ?? null
])
);
}
public function category($category_id)
{
$category = ArticleCategory::firstWhere('id', $category_id);
if ($category) {
$articles = Article::visible()
->where('category_id', $category_id)
->paginate($this->per_page);
return view(
'themes/' . $this->theme_directory . '/templates/index',
array_merge($this->data, [
'category' => $category,
'articles' => $articles
])
);
}
return redirect('/404');
}
public function show($slug)
{
$article = Article::visible()->where('slug', $slug)->firstOrFail();
$sessionKey = 'article_viewed_' . $article->id;
$lastViewedAt = session($sessionKey);
if (!$lastViewedAt || Carbon::createFromTimestamp($lastViewedAt)->diffInMinutes(now()) >= 60) {
$article->increment('views');
session()->put($sessionKey, now()->timestamp);
}
$old_article = Article::visible()
->where(function ($q) use ($article) {
$q->where('published_at', '<', $article->published_at)
->orWhere(function ($q2) use ($article) {
$q2->where('published_at', $article->published_at)
->where('id', '<', $article->id);
});
})
->orderBy('published_at', 'desc')
->orderBy('id', 'desc')
->first();
$new_article = Article::visible()
->where(function ($q) use ($article) {
$q->where('published_at', '>', $article->published_at)
->orWhere(function ($q2) use ($article) {
$q2->where('published_at', $article->published_at)
->where('id', '>', $article->id);
});
})
->orderBy('published_at', 'asc')
->orderBy('id', 'asc')
->first();
if ($this->is_infinitescroll) {
$comments = $this->get_commentQuery($article->id, $this->comments_per_page, 0)
->whereNull('parent_id')
->with(['user', 'replies.user'])
->get();
$comments_count = $this->get_commentQuery($article->id)->count();
} else {
$comments = $this->get_commentQuery($article->id)
->whereNull('parent_id')
->with(['user', 'replies.user'])
->get();
$comments_count = $this->get_commentQuery($article->id)->count();
}
return view(
'themes/' . $this->theme_directory . '/templates/single',
array_merge($this->data, [
'categories' => $this->article_categories,
'article' => $article,
'old_article' => $old_article,
'new_article' => $new_article,
'comments' => $comments,
'comments_count' => $comments_count,
'comments_per_page' => $this->comments_per_page,
'tagline' => $article->title,
'is_infinitescroll' => $this->is_infinitescroll
])
);
}
}
The 404 controller looks like this, and it sets the $title, $subtitle, and $message variables for the theme's 404 view, for everything but a not-found article.
class PageNotFoundController extends FrontendController
{
public function notfound()
{
return view(
'themes/' . $this->theme_directory . '/templates/404',
array_merge($this->data, [
'title' => "404",
'subtitle' => "Page not found :(",
'message' => "Nothing to see here!"
])
);
}
}
If an article is not found, that is handled by Handler.php.
The problem is an obvious repetition of code in app\Http\Controllers\FrontendController.php and app\Exceptions\Handler.php.
Question
- How can I avoid the code repetition mentioned above?
- Are there any security vulnerabilities in my code?
- Is there any room for optimization?