Improve exception handling and model binding

This commit is contained in:
Jonny Barnes 2020-08-09 15:54:10 +01:00
parent e9ca934cb4
commit 0fca80e7e4
22 changed files with 148 additions and 82 deletions

View file

@ -4,6 +4,7 @@ namespace App\Exceptions;
use Exception; use Exception;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
@ -23,7 +24,8 @@ class Handler extends ExceptionHandler
* @var array * @var array
*/ */
protected $dontReport = [ protected $dontReport = [
\Symfony\Component\HttpKernel\Exception\NotFoundHttpException::class, NotFoundHttpException::class,
ModelNotFoundException::class,
]; ];
/** /**
@ -49,35 +51,33 @@ class Handler extends ExceptionHandler
{ {
parent::report($throwable); parent::report($throwable);
if ($throwable instanceof NotFoundHttpException) { if ($this->shouldReport($throwable)) {
return; $guzzle = new Client([
} 'headers' => [
'Content-Type' => 'application/json',
],
]);
$guzzle = new Client([ $guzzle->post(
'headers' => [ config('logging.slack'),
'Content-Type' => 'application/json', [
], 'body' => json_encode([
]); 'attachments' => [[
'fallback' => 'There was an exception.',
$guzzle->post( 'pretext' => 'There was an exception.',
config('logging.slack'), 'color' => '#d00000',
[ 'author_name' => app()->environment(),
'body' => json_encode([ 'author_link' => config('app.url'),
'attachments' => [[ 'fields' => [[
'fallback' => 'There was an exception.', 'title' => get_class($throwable) ?? 'Unknown Exception',
'pretext' => 'There was an exception.', 'value' => $throwable->getTraceAsString() ?? '',
'color' => '#d00000', ]],
'author_name' => app()->environment(), 'ts' => time(),
'author_link' => config('app.url'),
'fields' => [[
'title' => get_class($throwable) ?? 'Unknown Exception',
'value' => $throwable->getTraceAsString() ?? '',
]], ]],
'ts' => time(), ]),
]], ]
]), );
] }
);
} }
/** /**

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Article; use App\Models\Article;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\View\View; use Illuminate\View\View;
use Jonnybarnes\IndieWeb\Numbers; use Jonnybarnes\IndieWeb\Numbers;
@ -14,8 +15,8 @@ class ArticlesController extends Controller
/** /**
* Show all articles (with pagination). * Show all articles (with pagination).
* *
* @param int $year * @param int|null $year
* @param int $month * @param int|null $month
* @return View * @return View
*/ */
public function index(int $year = null, int $month = null): View public function index(int $year = null, int $month = null): View
@ -38,7 +39,12 @@ class ArticlesController extends Controller
*/ */
public function show(int $year, int $month, string $slug) public function show(int $year, int $month, string $slug)
{ {
$article = Article::where('titleurl', $slug)->firstOrFail(); try {
$article = Article::where('titleurl', $slug)->firstOrFail();
} catch (ModelNotFoundException $exception) {
abort(404);
}
if ($article->updated_at->year != $year || $article->updated_at->month != $month) { if ($article->updated_at->year != $year || $article->updated_at->month != $month) {
return redirect('/blog/' return redirect('/blog/'
. $article->updated_at->year . $article->updated_at->year
@ -59,7 +65,11 @@ class ArticlesController extends Controller
public function onlyIdInUrl(int $idFromUrl): RedirectResponse public function onlyIdInUrl(int $idFromUrl): RedirectResponse
{ {
$realId = resolve(Numbers::class)->b60tonum($idFromUrl); $realId = resolve(Numbers::class)->b60tonum($idFromUrl);
$article = Article::findOrFail($realId); try {
$article = Article::findOrFail($realId);
} catch (ModelNotFoundException $exception) {
abort(404);
}
return redirect($article->link); return redirect($article->link);
} }

View file

@ -6,13 +6,14 @@ namespace App\Http\Controllers;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
class AuthController extends Controller class AuthController extends Controller
{ {
/** /**
* Show the login form. * Show the login form.
* *
* @return \Illuminate\View\View|\Illuminate\Http\RedirectResponse * @return View|RedirectResponse
*/ */
public function showLogin() public function showLogin()
{ {
@ -27,7 +28,7 @@ class AuthController extends Controller
* Log in a user, set a session variable, check credentials against * Log in a user, set a session variable, check credentials against
* the .env file. * the .env file.
* *
* @return \Illuminate\Http\RedirectResponse * @return RedirectResponse
*/ */
public function login(): RedirectResponse public function login(): RedirectResponse
{ {
@ -43,7 +44,7 @@ class AuthController extends Controller
/** /**
* Show the form to logout a user. * Show the form to logout a user.
* *
* @return \Illuminate\View\View|\Illuminate\Http\RedirectResponse * @return View|RedirectResponse
*/ */
public function showLogout() public function showLogout()
{ {
@ -58,7 +59,7 @@ class AuthController extends Controller
/** /**
* Log the user out from their current session. * Log the user out from their current session.
* *
* @return \Illuminate\Http\RedirectResponse; * @return RedirectResponse;
*/ */
public function logout(): RedirectResponse public function logout(): RedirectResponse
{ {

View file

@ -12,7 +12,7 @@ class BookmarksController extends Controller
/** /**
* Show the most recent bookmarks. * Show the most recent bookmarks.
* *
* @return \Illuminate\View\View * @return View
*/ */
public function index(): View public function index(): View
{ {
@ -24,8 +24,8 @@ class BookmarksController extends Controller
/** /**
* Show a single bookmark. * Show a single bookmark.
* *
* @param \App\Models\Bookmark $bookmark * @param Bookmark $bookmark
* @return \Illuminate\View\View * @return View
*/ */
public function show(Bookmark $bookmark): View public function show(Bookmark $bookmark): View
{ {

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Contact; use App\Models\Contact;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Filesystem\Filesystem; use Illuminate\Filesystem\Filesystem;
use Illuminate\View\View; use Illuminate\View\View;
@ -13,7 +14,7 @@ class ContactsController extends Controller
/** /**
* Show all the contacts. * Show all the contacts.
* *
* @return \Illuminate\View\View * @return View
*/ */
public function index(): View public function index(): View
{ {
@ -34,17 +35,15 @@ class ContactsController extends Controller
/** /**
* Show a single contact. * Show a single contact.
* *
* @todo Use implicit model binding. * @param Contact $contact
* * @return View
* @param string $nick The nickname associated with contact
* @return \Illuminate\View\View
*/ */
public function show(string $nick): View public function show(Contact $contact): View
{ {
$filesystem = new Filesystem();
$contact = Contact::where('nick', '=', $nick)->firstOrFail();
$contact->homepageHost = parse_url($contact->homepage, PHP_URL_HOST); $contact->homepageHost = parse_url($contact->homepage, PHP_URL_HOST);
$file = public_path() . '/assets/profile-images/' . $contact->homepageHost . '/image'; $file = public_path() . '/assets/profile-images/' . $contact->homepageHost . '/image';
$filesystem = new Filesystem();
$image = ($filesystem->exists($file)) ? $image = ($filesystem->exists($file)) ?
'/assets/profile-images/' . $contact->homepageHost . '/image' '/assets/profile-images/' . $contact->homepageHost . '/image'
: :

View file

@ -73,9 +73,9 @@ class FeedsController extends Controller
/** /**
* Returns the blog JSON feed. * Returns the blog JSON feed.
* *
* @return \Illuminate\Http\JsonResponse * @return array
*/ */
public function blogJson() public function blogJson(): array
{ {
$articles = Article::where('published', '1')->latest('updated_at')->take(20)->get(); $articles = Article::where('published', '1')->latest('updated_at')->take(20)->get();
$data = [ $data = [
@ -106,7 +106,7 @@ class FeedsController extends Controller
/** /**
* Returns the notes JSON feed. * Returns the notes JSON feed.
* *
* @return \Illuminate\Http\JsonResponse * @return array
*/ */
public function notesJson() public function notesJson()
{ {

View file

@ -7,11 +7,15 @@ use App\Models\Bookmark;
use App\Models\Like; use App\Models\Like;
use App\Models\Note; use App\Models\Note;
use App\Services\ActivityStreamsService; use App\Services\ActivityStreamsService;
use Illuminate\Http\Response;
use Illuminate\View\View;
class FrontPageController extends Controller class FrontPageController extends Controller
{ {
/** /**
* Show all the recent activity. * Show all the recent activity.
*
* @return Response|View
*/ */
public function index() public function index()
{ {
@ -19,8 +23,6 @@ class FrontPageController extends Controller
return (new ActivityStreamsService())->siteOwnerResponse(); return (new ActivityStreamsService())->siteOwnerResponse();
} }
$pageNumber = request()->query('page') ?? 1;
$notes = Note::latest()->get(); $notes = Note::latest()->get();
$articles = Article::latest()->get(); $articles = Article::latest()->get();
$bookmarks = Bookmark::latest()->get(); $bookmarks = Bookmark::latest()->get();

View file

@ -12,7 +12,7 @@ class LikesController extends Controller
/** /**
* Show the latest likes. * Show the latest likes.
* *
* @return \Illuminate\View\View * @return View
*/ */
public function index(): View public function index(): View
{ {
@ -24,8 +24,8 @@ class LikesController extends Controller
/** /**
* Show a single like. * Show a single like.
* *
* @param \App\Models\Like $like * @param Like $like
* @return \Illuminate\View\View * @return View
*/ */
public function show(Like $like): View public function show(Like $like): View
{ {

View file

@ -16,10 +16,10 @@ use MStaack\LaravelPostgis\Geometries\Point;
class MicropubController extends Controller class MicropubController extends Controller
{ {
protected $tokenService; protected TokenService $tokenService;
protected $hentryService; protected HEntryService $hentryService;
protected $hcardService; protected HCardService $hcardService;
protected $updateService; protected UpdateService $updateService;
public function __construct( public function __construct(
TokenService $tokenService, TokenService $tokenService,

View file

@ -6,7 +6,7 @@ namespace App\Http\Controllers;
use App\Models\Note; use App\Models\Note;
use App\Services\ActivityStreamsService; use App\Services\ActivityStreamsService;
use Illuminate\Contracts\View\Factory as ViewFactory; use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Response; use Illuminate\Http\Response;
@ -20,7 +20,7 @@ class NotesController extends Controller
/** /**
* Show all the notes. This is also the homepage. * Show all the notes. This is also the homepage.
* *
* @return ViewFactory|View|Response * @return View|Response
*/ */
public function index() public function index()
{ {
@ -45,7 +45,11 @@ class NotesController extends Controller
*/ */
public function show(string $urlId) public function show(string $urlId)
{ {
$note = Note::nb60($urlId)->with('webmentions')->firstOrFail(); try {
$note = Note::nb60($urlId)->with('webmentions')->firstOrFail();
} catch (ModelNotFoundException $exception) {
abort(404);
}
if (request()->wantsActivityStream()) { if (request()->wantsActivityStream()) {
return (new ActivityStreamsService())->singleNoteResponse($note); return (new ActivityStreamsService())->singleNoteResponse($note);

View file

@ -12,7 +12,7 @@ class PlacesController extends Controller
/** /**
* Show all the places. * Show all the places.
* *
* @return \Illuminate\View\View * @return View
*/ */
public function index(): View public function index(): View
{ {
@ -24,13 +24,11 @@ class PlacesController extends Controller
/** /**
* Show a specific place. * Show a specific place.
* *
* @param string $slug * @param Place $place
* @return \Illuminate\View\View * @return View
*/ */
public function show(string $slug): View public function show(Place $place): View
{ {
$place = Place::where('slug', '=', $slug)->firstOrFail();
return view('singleplace', ['place' => $place]); return view('singleplace', ['place' => $place]);
} }
} }

View file

@ -12,7 +12,7 @@ class SearchController extends Controller
/** /**
* Display search results. * Display search results.
* *
* @return \Illuminate\View\View * @return View
*/ */
public function search(): View public function search(): View
{ {

View file

@ -9,9 +9,9 @@ class SessionStoreController extends Controller
/** /**
* Save the selected colour scheme in the session. * Save the selected colour scheme in the session.
* *
* @return \Illuminate\Http\JsonResponse * @return string[]
*/ */
public function saveColour() public function saveColour(): array
{ {
$css = request()->input('css'); $css = request()->input('css');

View file

@ -20,7 +20,7 @@ class ShortURLsController extends Controller
/** /**
* Redirect from '/' to the long url. * Redirect from '/' to the long url.
* *
* @return \Illuminate\Http\RedirectResponse * @return RedirectResponse
*/ */
public function baseURL(): RedirectResponse public function baseURL(): RedirectResponse
{ {
@ -30,7 +30,7 @@ class ShortURLsController extends Controller
/** /**
* Redirect from '/@' to a twitter profile. * Redirect from '/@' to a twitter profile.
* *
* @return \Illuminate\Http\RedirectResponse * @return RedirectResponse
*/ */
public function twitter(): RedirectResponse public function twitter(): RedirectResponse
{ {
@ -40,7 +40,7 @@ class ShortURLsController extends Controller
/** /**
* Redirect from '/+' to a Google+ profile. * Redirect from '/+' to a Google+ profile.
* *
* @return \Illuminate\Http\RedirectResponse * @return RedirectResponse
*/ */
public function googlePlus(): RedirectResponse public function googlePlus(): RedirectResponse
{ {
@ -53,7 +53,7 @@ class ShortURLsController extends Controller
* *
* @param string Post type * @param string Post type
* @param string Post ID * @param string Post ID
* @return \Illuminate\Http\RedirectResponse * @return RedirectResponse
*/ */
public function expandType(string $type, string $postId): RedirectResponse public function expandType(string $type, string $postId): RedirectResponse
{ {

View file

@ -13,18 +13,18 @@ class TokenEndpointController extends Controller
/** /**
* The IndieAuth Client. * The IndieAuth Client.
*/ */
protected $client; protected Client $client;
/** /**
* The Token handling service. * The Token handling service.
*/ */
protected $tokenService; protected TokenService $tokenService;
/** /**
* Inject the dependencies. * Inject the dependencies.
* *
* @param \IndieAuth\Client $client * @param Client $client
* @param \App\Services\TokenService $tokenService * @param TokenService $tokenService
*/ */
public function __construct( public function __construct(
Client $client, Client $client,
@ -37,7 +37,7 @@ class TokenEndpointController extends Controller
/** /**
* If the user has authd via the IndieAuth protocol, issue a valid token. * If the user has authd via the IndieAuth protocol, issue a valid token.
* *
* @return \Illuminate\Http\JsonResponse * @return JsonResponse
*/ */
public function create(): JsonResponse public function create(): JsonResponse
{ {

View file

@ -60,6 +60,16 @@ class Place extends Model
use Sluggable; use Sluggable;
use PostgisTrait; use PostgisTrait;
/**
* Get the route key for the model.
*
* @return string
*/
public function getRouteKeyName()
{
return 'slug';
}
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.
* *

View file

@ -156,11 +156,11 @@ Route::group(['domain' => config('url.longurl')], function () {
// Contacts // Contacts
Route::get('contacts', 'ContactsController@index'); Route::get('contacts', 'ContactsController@index');
Route::get('contacts/{nick}', 'ContactsController@show'); Route::get('contacts/{contact:nick}', 'ContactsController@show');
// Places // Places
Route::get('places', 'PlacesController@index'); Route::get('places', 'PlacesController@index');
Route::get('places/{slug}', 'PlacesController@show'); Route::get('places/{place}', 'PlacesController@show');
Route::get('search', 'SearchController@search'); Route::get('search', 'SearchController@search');

View file

@ -30,4 +30,18 @@ class ArticlesTest extends TestCase
$response = $this->get('/blog/s/2'); $response = $this->get('/blog/s/2');
$response->assertRedirect('/blog/' . date('Y') . '/' . date('m') . '/some-code-i-did'); $response->assertRedirect('/blog/' . date('Y') . '/' . date('m') . '/some-code-i-did');
} }
/** @test */
public function unknownSlugGives404()
{
$response = $this->get('/blog/' . date('Y') . '/' . date('m') . '/unknown-slug');
$response->assertNotFound();
}
/** @test */
public function unknownArticleIdGives404()
{
$response = $this->get('/blog/s/22');
$response->assertNotFound();
}
} }

View file

@ -38,4 +38,11 @@ class ContactsTest extends TestCase
$response = $this->get('/contacts/aaron'); $response = $this->get('/contacts/aaron');
$response->assertViewHas('image', '/assets/profile-images/aaronparecki.com/image'); $response->assertViewHas('image', '/assets/profile-images/aaronparecki.com/image');
} }
/** @test */
public function unknownContactGives404()
{
$response = $this->get('/contacts/unknown');
$response->assertNotFound();
}
} }

View file

@ -211,4 +211,11 @@ END;
$this->assertEquals('Jonny Barnes', Like::find($id)->author_name); $this->assertEquals('Jonny Barnes', Like::find($id)->author_name);
} }
/** @test */
public function unknownLikeGives404()
{
$response = $this->get('/likes/202');
$response->assertNotFound();
}
} }

View file

@ -57,4 +57,11 @@ class NotesControllerTest extends TestCase
$response = $this->get('/notes/tagged/beer'); $response = $this->get('/notes/tagged/beer');
$response->assertViewHas('tag', 'beer'); $response->assertViewHas('tag', 'beer');
} }
/** @test */
public function unknownNoteGives404()
{
$response = $this->get('/notes/112233');
$response->assertNotFound();
}
} }

View file

@ -29,4 +29,11 @@ class PlacesTest extends TestCase
$response = $this->get('/places/the-bridgewater-pub'); $response = $this->get('/places/the-bridgewater-pub');
$response->assertViewHas('place', $place); $response->assertViewHas('place', $place);
} }
/** @test */
public function unknownPlaceGives404()
{
$response = $this->get('/places/unknown');
$response->assertNotFound();
}
} }