A Laravel Project Start to Finish: Part 1 - Object-Oriented Design & TDD

Dec 23rd, 2019

This is a series of posts I will be writing with the intention to document a project from start to finish using MySQL, Laravel, Vue.js and tailwindcss. I'm talking about taking a project from only an idea to a production website used by people. I am choosing to keep the project in a private repository because I don't really want my proprietary content to be public, and, when looking at similar businesses, they do the same thing. But that doesn't mean I can't document and share as much as I can with you about the process of building it!

And with that I unveil my Secret Projectâ„¢, which is going to be a site I launch with the purpose of teaching through screencasting! I plan on developing courses that contain all different types of resources including screencasts, exercises, projects, comments, documents and external resources, and more! The plan is to be able to give away as much free content as I can get away with because just being able to teach is a great privilege that will teach me even more than I can teach you. I can't share many details just yet because this idea isn't concrete yet and I don't want to commit to any ideas right now. All I know is I need a dedicated site where I can create courses to share with an audience that wants to learn the things that I am knowledgeable enough about to teach.

Now, let's get into the project.

Setting up the Environment

Now, I don't want to get far into the weeds of setting up an environment for your project because that depends on a lot of different factors, so the best I can do is share with you what I'm working with. Luckily there is an abundance of options with a ton of documentation on how to handle that.

  • Operating System: Windows 10
  • IDE: VS Code

First, you need Composer installed and added to your path variable, so it is usable as a command from the command line. With that you can install Laravel so it is usable as a command as well. If you need direction on doing that, you can find that here: https://laravel.com/docs/6.x.

You also need to setup MySQL on your machine. I personally use MAMP to handle the database connection and MySQL Workbench to interact with the database.

Beginning the Application

Okay, now we can actually get our application up and running. First things first, create a new Laravel application:

1$ laravel new <app_name>

Handling the Database

Whenever I'm working with a new project I like to create a separate user in the database just for interacting with that project for security purposes. Add that information into your .env file so you can connect with the database:

1DB_CONNECTION=mysql
2DB_HOST=127.0.0.1
3DB_PORT=3306
4DB_DATABASE=testdb
5DB_USERNAME="testusername"
6DB_PASSWORD="testpassword"

Pro Tip: Use quotations around the username and password because certain characters will break the migration command.

Then run the command below to migrate the tables that come with the project to test that your connection is working as expected. Those migration files are found in database/migrations/.

1$ php artisan migrate

The next thing you can do is run the below command and go to http://localhost:8000 to see if your project is being served up.

1$ php artisan serve

At this point if you database is hooked up and you can see your project being served in the browser, you are ready to start working on the project. But not so fast! I didn't even start my project until I spent a hefty amount of time just designing what the application should do.

Designing the Application

I have a Github repository dedicated to object-oriented design, so if you want to go into more detail on what I've done you can probably find it there. I also recommend the course itself.

I began with getting the idea for the project out on paper. I didn't go super crazy with it, but I wrote out some use cases and user stories so I could collect objects and behaviors and begin to see the bigger picture.

Imgur

As you can see, I highlighted nouns in blue and behaviors in orange. I also added user stories--as I wrote these, thinking from the user perspective gave me a lot of ideas for more things I would want as a user. I think the exercise was beneficial for really fleshing out the ideas and what was necessary for the application to be successful.

Imgur

I gathered up my objects and their behaviors and listed them out. On paper I connected the objects to each other to see their relationships.

TDD

Funny, but I haven't actually started writing the actual application yet, or at least anything that can be seen from the browser. I have only started writing tests.

At this point I have begun writing the models and migrations for the objects that will be in the application. It's a little all over the place, but I started with the things that were easy, such as the relationships between a user, a course, and a lesson.

1// app/User.php
2namespace App;
3 
4use Illuminate\Contracts\Auth\MustVerifyEmail;
5use Illuminate\Foundation\Auth\User as Authenticatable;
6use Illuminate\Notifications\Notifiable;
7use App\Course;
8use App\Lesson;
9 
10class User extends Authenticatable
11{
12 use Notifiable;
13 
14 /**
15 * The attributes that are mass assignable.
16 *
17 * @var array
18 */
19 protected $fillable = [
20 'name', 'email', 'password',
21 ];
22 
23 /**
24 * The attributes that should be hidden for arrays.
25 *
26 * @var array
27 */
28 protected $hidden = [
29 'password', 'remember_token',
30 ];
31 
32 /**
33 * The attributes that should be cast to native types.
34 *
35 * @var array
36 */
37 protected $casts = [
38 'email_verified_at' => 'datetime',
39 ];
40 
41 public function register(Course $course)
42 {
43 $this->courses()->save($course);
44 }
45 
46 public function courses()
47 {
48 return $this->belongsToMany(Course::class);
49 }
50 
51 public function completedLessons()
52 {
53 return $this->belongsToMany(Lesson::class, 'completed_lessons', 'user_id', 'lesson_id');
54 }
55}
1// tests/Feature/UserTest.php
2namespace Tests\Feature;
3 
4use Illuminate\Foundation\Testing\RefreshDatabase;
5use Illuminate\Foundation\Testing\WithFaker;
6use Tests\TestCase;
7use \App\Course;
8use \App\User;
9 
10class UserTest extends TestCase
11{
12 use RefreshDatabase;
13 
14 /** @test */
15 function a_user_can_be_authenticated()
16 {
17 $user = factory(User::class)->create();
18 
19 $this->actingAs($user);
20 
21 $this->assertAuthenticatedAs($user);
22 }
23 
24 /** @test */
25 function a_user_can_register_for_a_course()
26 {
27 $user = factory(User::class)->create();
28 
29 $this->actingAs($user);
30 
31 $course = factory(Course::class)->create();
32 
33 $user->register($course);
34 
35 $user->courses;
36 
37 $this->assertDatabaseHas('course_user', [
38 'course_id' => $course->id,
39 'user_id' => $user->id,
40 ]);
41 }
42}

I started with this test:

1// tests/Feature/UserTest.php
2/** @test */
3function a_user_can_be_authenticated()
4{
5 $user = factory(User::class)->create();
6 
7 $this->actingAs($user);
8 
9 $this->assertAuthenticatedAs($user);
10}

I think this test passed immediately because the user model and migration was already setup from the start. So the next test was:

1/** @test */
2function a_user_can_register_for_a_course()
3{
4 $user = factory(User::class)->create();
5 
6 $this->actingAs($user);
7 
8 $course = factory(Course::class)->create();
9 
10 $user->register($course);
11 
12 $user->courses;
13 
14 $this->assertDatabaseHas('course_user', [
15 'course_id' => $course->id,
16 'user_id' => $user->id,
17 ]);
18}

This failed because I didn't have a Course model, a factory for that model, the relationship defined on those models, or a register method for the user. I started with the model:

1$ php artisan make:migration create_courses_table -m

This command creates the migration file for creating the courses table. The -m flag creates the model for it simultaneously. First I defined the columns for my courses table in the migration file:

1use Illuminate\Database\Migrations\Migration;
2use Illuminate\Database\Schema\Blueprint;
3use Illuminate\Support\Facades\Schema;
4 
5class CreateCoursesTable extends Migration
6{
7 /**
8 * Run the migrations.
9 *
10 * @return void
11 */
12 public function up()
13 {
14 Schema::create('courses', function (Blueprint $table) {
15 $table->bigIncrements('id');
16 $table->string('name');
17 $table->text('description');
18 $table->integer('price')->default(0);
19 $table->timestamps();
20 });
21 }
22 
23 /**
24 * Reverse the migrations.
25 *
26 * @return void
27 */
28 public function down()
29 {
30 Schema::dropIfExists('courses');
31 }
32}

Then I created the factory function:

1$factory->define(Course::class, function (Faker $faker) {
2 return [
3 'name' => $faker->sentence,
4 'description' => $faker->paragraph,
5 'price' => 0,
6 ];
7});

Next, I needed to define the relationships on the User and Course models.

1// app/User.php
2public function courses()
3{
4 return $this->belongsToMany(Course::class);
5}
1// app/Course.php
2public function users()
3{
4 return $this->belongsToMany(User::class);
5}

In a many-to-many relationship we use the belongsToMany() method on both objects. For that, we also need a pivot table.

1$ php artisan make:migration create_course_user_table

In that migration we give the foreign keys of both the course and the user:

1<?php
2 
3use Illuminate\Database\Migrations\Migration;
4use Illuminate\Database\Schema\Blueprint;
5use Illuminate\Support\Facades\Schema;
6 
7class CreateCourseUserTable extends Migration
8{
9 /**
10 * Run the migrations.
11 *
12 * @return void
13 */
14 public function up()
15 {
16 Schema::create('course_user', function (Blueprint $table) {
17 $table->bigInteger('course_id');
18 $table->bigInteger('user_id');
19 $table->timestamps();
20 $table->primary(['course_id', 'user_id']);
21 });
22 }
23 
24 /**
25 * Reverse the migrations.
26 *
27 * @return void
28 */
29 public function down()
30 {
31 Schema::dropIfExists('course_user');
32 }
33}

Now that we have the pivot table, the models, and their relationships defined, the last step is to create the register() method on the user so our test will pass.

1public function register(Course $course)
2{
3 $this->courses()->save($course);
4}

This may seem like a stupid method to write when it's only using the existing save() method to create the relationship, but really what we are creating is a wrapper that uses the language that makes sense. A user doesn't save a course, they register for it, and having that clarity in our code is important when we come back to it further down the road.

Not only that, but right now register is pretty simple, but what if register grows in complexity as our app becomes more mature? It'll be nice that the logic was handled within the method so we don't have to find all of the places we are registering and change it.

In Part 2

In part 2 I plan on designing the actual style of the application through mock-ups so I can begin getting the front-end setup. We will add Vue, tailwindcss with our custom configuration, and begin setting up our routes. Thanks for reading!