Most websites that allow a user to sign up and contribute information have a profile page for each user. This application is no different. In this tutorial what we want to do is allow users of the website to click on a user name in the browser, and see more information about that user on their profile page. We’ll setup a basic profile page feature with supporting test cases in our code to make sure it works well. As with the prior tutorials, we will let the results of running our tests direct us on what the next steps to take are when building out the profile page for our users.
Create a User Profile Test
Navigating to the Feature folder of our application, we can add a new Test Class named ProfilesTest. Then go ahead and add the boilerplate for a new test.
Create The Test Pseudocode
- Given we have a user in the application
- When we visit the profile / their name
- Then we should see their name on the page
Ok sounds reasonable enough. Let’s translate that into a test method and associated code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<?php namespace TestsFeature; use IlluminateFoundationTestingDatabaseMigrations; use TestsTestCase; use IlluminateFoundationTestingWithFaker; use IlluminateFoundationTestingRefreshDatabase; class ProfilesTest extends TestCase { use DatabaseMigrations; public function test_a_user_has_a_profile() { $user = create('AppUser'); $this->withoutExceptionHandling()->get('/profiles/' . $user->name) ->assertSee($user->name); } } |
Let’s start running the test, and letting the errors direct us on the next steps to take.
vagrant@homestead:~/Code/forumio$ phpunit --filter test_a_user_has_a_profile PHPUnit 6.5.5 by Sebastian Bergmann and contributors. E 1 / 1 (100%) Time: 947 ms, Memory: 8.00MB There was 1 error: 1) TestsFeatureProfilesTest::test_a_user_has_a_profile SymfonyComponentHttpKernelExceptionNotFoundHttpException: GET http://localhost/profiles/Athena Zulauf /home/vagrant/Code/forumio/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/InteractsWithExceptionHandling.php:107 /home/vagrant/Code/forumio/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php:326 /home/vagrant/Code/forumio/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php:120 /home/vagrant/Code/forumio/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php:345 /home/vagrant/Code/forumio/vendor/laravel/framework/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php:168 /home/vagrant/Code/forumio/tests/Feature/ProfilesTest.php:18 ERRORS! Tests: 1, Assertions: 0, Errors: 1.
We are getting a NotFoundHttpException which of course means there is no route to support that request.
Add the necessary route
Open the web.php routes file, and let’s add the route we need as highlighted here. When a user makes a request to /profiles/the user, then the show() method of the ProfilesController should trigger.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<?php Route::get('/', function () { return view('welcome'); }); Auth::routes(); Route::get('/home', 'HomeController@index')->name('home'); Route::get('/threads', 'ThreadsController@index'); Route::get('/threads/create', 'ThreadsController@create'); Route::get('/threads/{channel}/{thread}', 'ThreadsController@show'); Route::post('/threads', 'ThreadsController@store'); Route::get('/threads/{channel}', 'ThreadsController@index'); Route::post('/threads/{channel}/{thread}/replies', 'RepliesController@store'); Route::post('/replies/{reply}/favorites', 'FavoritesController@store'); Route::get('/profiles/{user}', 'ProfilesController@show'); |
Run the test again to see what the error is.
vagrant@homestead:~/Code/forumio$ phpunit --filter test_a_user_has_a_profile PHPUnit 6.5.5 by Sebastian Bergmann and contributors. E 1 / 1 (100%) Time: 960 ms, Memory: 8.00MB There was 1 error: 1) TestsFeatureProfilesTest::test_a_user_has_a_profile ReflectionException: Class AppHttpControllersProfilesController does not exist
The test gives an error that the ProfilesController does not exist. This does make sense of course, since we have not yet created the controller we need to support profiles.
Create A ProfilesController Class
We can quickly create a new controller with this snippet.
vagrant@homestead:~/Code/forumio$ php artisan make:controller ProfilesController Controller created successfully.
Since we now have a ProfilesController in place, that error should be fixed in the test. Let’s test again.
vagrant@homestead:~/Code/forumio$ phpunit --filter test_a_user_has_a_profile PHPUnit 6.5.5 by Sebastian Bergmann and contributors. E 1 / 1 (100%) Time: 918 ms, Memory: 8.00MB There was 1 error: 1) TestsFeatureProfilesTest::test_a_user_has_a_profile BadMethodCallException: Method [show] does not exist on [AppHttpControllersProfilesController].
Now we are getting a BadMethodCallException. Makes sense of course, since there is no show() method on the controller.
Create a show() method
Now we can add the show() method to the ProfilesController class. Here we go:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<?php namespace AppHttpControllers; use IlluminateHttpRequest; class ProfilesController extends Controller { public function show() { } } |
The show method does exist now. It doesn’t do anything yet, but let’s run our test to see what the message is now.
vagrant@homestead:~/Code/forumio$ phpunit --filter test_a_user_has_a_profile PHPUnit 6.5.5 by Sebastian Bergmann and contributors. F 1 / 1 (100%) Time: 881 ms, Memory: 8.00MB There was 1 failure: 1) TestsFeatureProfilesTest::test_a_user_has_a_profile Failed asserting that '' contains "Forrest Treutel". /home/vagrant/Code/forumio/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestResponse.php:236 /home/vagrant/Code/forumio/tests/Feature/ProfilesTest.php:19 FAILURES! Tests: 1, Assertions: 1, Failures: 1.
Looks like our good friend Forrest Treutel is not being seen on the page when we visit his profile. Let’s fix up that show() method to fix this error.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<?php namespace AppHttpControllers; use IlluminateHttpRequest; class ProfilesController extends Controller { public function show() { return view('profiles.show'); } } |
Create A View For User Profiles
The show.blade.php for profiles does not yet exist, so we need to create that. Just for grins, let’s confirm that with our test.
vagrant@homestead:~/Code/forumio$ phpunit --filter test_a_user_has_a_profile PHPUnit 6.5.5 by Sebastian Bergmann and contributors. E 1 / 1 (100%) Time: 861 ms, Memory: 8.00MB There was 1 error: 1) TestsFeatureProfilesTest::test_a_user_has_a_profile InvalidArgumentException: View [profiles.show] not found.
Yep. The view profiles.show is not found. Let’s create that view.
We’ll start with this basic markup.
1 2 3 4 5 |
@extends('layouts.app') @section('content') {{ $profileUser->name }} @endsection |
Let’s give that test another whirl.
vagrant@homestead:~/Code/forumio$ phpunit --filter test_a_user_has_a_profile PHPUnit 6.5.5 by Sebastian Bergmann and contributors. E 1 / 1 (100%) Time: 1.26 seconds, Memory: 10.00MB There was 1 error: 1) TestsFeatureProfilesTest::test_a_user_has_a_profile ErrorException: Undefined variable: profileUser (View: /home/vagrant/Code/forumio/resources/views/profiles/show.blade.php)
It looks like we have an undefined variable. We need to pass data to the view to fix this. To do this, we can update the show() method in the ProfilesController like so.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<?php namespace AppHttpControllers; use IlluminateHttpRequest; class ProfilesController extends Controller { public function show(User $user) { return view('profiles.show', [ 'profileUser' => $user ]); } } |
Now that we are sharing our variable, we can run the test to see if we fixed it.
vagrant@homestead:~/Code/forumio$ phpunit --filter test_a_user_has_a_profile PHPUnit 6.5.5 by Sebastian Bergmann and contributors. E 1 / 1 (100%) Time: 911 ms, Memory: 8.00MB There was 1 error: 1) TestsFeatureProfilesTest::test_a_user_has_a_profile IlluminateDatabaseEloquentModelNotFoundException: No query results for model [AppUser].
So we fixed the previous error, but now we are getting a ModelNotFoundException. The reason is in how Route Model Binding is configured by default.
Configuring Route Model Binding Route Key
If we want the Model to make use of a slug, like we are doing here, then we need to configure the getRouteKeyName() method to support this. Go ahead and open up the User Model, and we can make this adjustment.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<?php namespace App; use IlluminateNotificationsNotifiable; use IlluminateFoundationAuthUser as Authenticatable; class User extends Authenticatable { use Notifiable; protected $fillable = [ 'name', 'email', 'password', ]; protected $hidden = [ 'password', 'remember_token', ]; public function getRouteKeyName() { return 'name'; } } |
Once again, let’s run the test. Yahoo!! Look at that GREEN!
This means that if we were to visit a profile page in the Browser, it should actually work at this point. There just so happens to be a user in the system named “Tom” so let’s visit http://forum.io/profiles/tom and see what we get.
Better Styling With Bootstrap
So we see the user name on the page, and we’re not exactly blown away by the design. The logic of the page works just fine, but we want to make things look a little more pleasing. We can update the show.blade.php view to something like this:
1 2 3 4 5 6 7 8 9 10 11 12 |
@extends('layouts.app') @section('content') <div class="container"> <div class="page-header"> <h1> {{ $profileUser->name }} <small>registered {{ $profileUser->created_at->diffForHumans() }}</small> </h1> </div> </div> @endsection |
which gives a little nicer profile layout.
Displaying Threads A User Has Created
One feature we would like to add is the ability to view all threads created by the user who’s profile you are viewing. In order to add this feature, we will once again build out a test first to support it. In the ProfilesTest class, we can add a new test method of test_profiles_display_all_threads_by_the_user(). Now let’s think of what we want the test to accomplish.
- Given we have a user
- Given we have a thread created by that user
- When we visit their profile page
- Then we should see their thread
Here is some test code that approximates these steps.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
<?php namespace TestsFeature; use IlluminateFoundationTestingDatabaseMigrations; use TestsTestCase; use IlluminateFoundationTestingWithFaker; use IlluminateFoundationTestingRefreshDatabase; class ProfilesTest extends TestCase { use DatabaseMigrations; public function test_a_user_has_a_profile() { $user = create('AppUser'); $this->withoutExceptionHandling()->get('/profiles/' . $user->name) ->assertSee($user->name); } public function test_profiles_display_all_threads_by_the_user() { $user = create('AppUser'); $thread = create('AppThread', ['user_id' => $user->id]); $this->withoutExceptionHandling()->get('/profiles/' . $user->name) ->assertSee($thread->title) ->assertSee($thread->body); } } |
With our test in place we can trigger it to run and observe the results.
vagrant@homestead:~/Code/forumio$ phpunit --filter test_profiles_display_all_threads_by_the_user PHPUnit 6.5.5 by Sebastian Bergmann and contributors. F 1 / 1 (100%) Time: 1.22 seconds, Memory: 10.00MB There was 1 failure: 1) TestsFeatureProfilesTest::test_profiles_display_all_threads_by_the_user Failed asserting that '(the html)' contains "Alias autem velit atque sit aspernatur.". /home/vagrant/Code/forumio/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestResponse.php:236 /home/vagrant/Code/forumio/tests/Feature/ProfilesTest.php:29 FAILURES! Tests: 1, Assertions: 1, Failures: 1.
The test is failing because the title of the thread is actually not appearing on the profile page. We need to build out the code that actually makes that happen. We can add this block of markup to show.blade.php to display the threads of the user.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
@extends('layouts.app') @section('content') <div class="container"> <div class="page-header"> <h1> {{ $profileUser->name }} <small>registered {{ $profileUser->created_at->diffForHumans() }}</small> </h1> </div> @foreach($profileUser->threads as $thread) <div class="panel panel-default"> <div class="panel-heading"> <a href="#">{{ $thread->creator->name }}</a> posted: {{ $thread->title }} </div> <div class="panel-body"> {{ $thread->body }} </div> </div> @endforeach </div> @endsection |
Once again, we will run our test to see how it works.
vagrant@homestead:~/Code/forumio$ phpunit --filter test_profiles_display_all_threads_by_the_user PHPUnit 6.5.5 by Sebastian Bergmann and contributors. E 1 / 1 (100%) Time: 835 ms, Memory: 10.00MB There was 1 error: 1) TestsFeatureProfilesTest::test_profiles_display_all_threads_by_the_user ErrorException: Invalid argument supplied for foreach() (View: /home/vagrant/Code/forumio/resources/views/profiles/show.blade.php)
The error is complaining of an invalid argument being passed to the foreach loop. Well, that makes sense actually since we did not yet create the threads relationship on the User Model. We can add that right now. Also note that we add the latest() method to the relation definition so that we we are viewing the user’s threads, the most recent threads appear at the top of the page.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
<?php namespace App; use IlluminateNotificationsNotifiable; use IlluminateFoundationAuthUser as Authenticatable; class User extends Authenticatable { use Notifiable; protected $fillable = [ 'name', 'email', 'password', ]; protected $hidden = [ 'password', 'remember_token', ]; public function getRouteKeyName() { return 'name'; } public function threads() { return $this->hasMany(Thread::class)->latest(); } } |
Running our test shows that it is now passing.
vagrant@homestead:~/Code/forumio$ phpunit --filter test_profiles_display_all_threads_by_the_user PHPUnit 6.5.5 by Sebastian Bergmann and contributors. . 1 / 1 (100%) Time: 892 ms, Memory: 10.00MB OK (1 test, 2 assertions)
When we now visit our sample user Tom’s profile page at http://forum.io/profiles/tom, we do see a recent thread that he created! So it looks like viewing threads on the user’s profile page is now good to go.
Paginating Threads On User Profile Page
As users spend more time posting threads on the application, they may amass hundreds and hundreds of threads created. We should paginate these on the profile page instead of trying to load all of them in one shot. This is easy to set up in the show() method of the ProfilesController, as well as the associated view file with a call to $threads->links().
Updating the controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<?php namespace AppHttpControllers; use AppUser; use IlluminateHttpRequest; class ProfilesController extends Controller { public function show(User $user) { return view('profiles.show', [ 'profileUser' => $user, 'threads' => $user->threads()->paginate(1) ]); } } |
Updating the view
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
@extends('layouts.app') @section('content') <div class="container"> <div class="page-header"> <h1> {{ $profileUser->name }} <small>registered {{ $profileUser->created_at->diffForHumans() }}</small> </h1> </div> @foreach($threads as $thread) <div class="panel panel-default"> <div class="panel-heading"> <div class="level"> <span class="flex"> <a href="#">{{ $thread->creator->name }}</a> posted: {{ $thread->title }} </span> <span>{{$thread->created_at->diffForHumans()}}</span> </div> </div> <div class="panel-body"> {{ $thread->body }} </div> </div> @endforeach {{ $threads->links() }} </div> @endsection |
Now the pagination is working nicely. We set it to only 1 at this point just to demonstrate. Maybe you could bump it up to 20 or 30 in the real application.
Linking User Names To Their Profile Page
At this point the user names displayed in our view files do not link anywhere. Let’s update that so that users can click on a user name in the browser, and be brought to their profile page.
In show.blade.php you can swap instances of the first link with the second link.
1 2 3 |
<a href="#">{{ $thread->creator->name }}</a> <a href="/profiles/{{ $thread->creator->name }}">{{ $thread->creator->name }}</a> |
In reply.blade.php you can swap you can swap instances of the first link with the second link.
1 2 3 |
<a href="#">{{$reply->owner->name}}</a> <a href="/profiles/{{ $reply->owner->name }}">{{$reply->owner->name}}</a> |
Now all the instances of a user name in the browser link back to the user’s profile.
Making Use Of Named Routes
One other option you have in this case is to set up a named route if you like. This way you can use the named route in the href of your anchor tags of the view files. How does this work? Well first off, we need to assign a name to the route in question. We do this in our routes file with the example highlighted here. We are naming this route ‘profile’.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<?php Route::get('/', function () { return view('welcome'); }); Auth::routes(); Route::get('/home', 'HomeController@index')->name('home'); Route::get('/threads', 'ThreadsController@index'); Route::get('/threads/create', 'ThreadsController@create'); Route::get('/threads/{channel}/{thread}', 'ThreadsController@show'); Route::post('/threads', 'ThreadsController@store'); Route::get('/threads/{channel}', 'ThreadsController@index'); Route::post('/threads/{channel}/{thread}/replies', 'RepliesController@store'); Route::post('/replies/{reply}/favorites', 'FavoritesController@store'); Route::get('/profiles/{user}', 'ProfilesController@show')->name('profile'); |
The nice thing about named routes is that you can continue to use the regular links as we have already configured. Additionally however, we can format our links using the name of the route like we see here.
In show.blade.php you can swap instances of the first link with the second link.
1 2 |
<a href="/profiles/{{ $thread->creator->name }}"> <a href="{{ route('profile', $thread->creator) }}"> |
In reply.blade.php you can swap instances of the first link with the second link.
1 2 |
<a href="/profiles/{{ $reply->owner->name }}"> <a href="{{ route('profile', $reply->owner) }}"> |
How To Create User Profiles In Your Application Summary
What a great tutorial we had here! There were a lot of steps to take in getting our user profile page for users set up and working correctly. Thankfully, we took our time and got everything working nicely.