Courses

Re-creating Booking.com API with Laravel and PHPUnit

Profile Fields Structure

Let's take a look at another side of the user system: profile fields. Some of the fields are in this screenshot:

Booking profile fields

The main question is where do we store them and how to structure the tables, for multiple user roles?

In other words, should the property owner profile fields be in the same table as simple user fields, or do we need separate tables?


Goals of This Lesson

  • Think about different ways of structuring profiles in DB
  • Add the necessary fields
  • Look at a different project scenario: doctors/patients

By the end of this lesson, we will have this DB structure:

User profile DB schema


Users Table or Profile Table?

After browsing through the booking.com website, mobile app, and help docs, I've identified these profile fields for each user role.

Notice: we don't really need any profile fields for administrators, as they are not visible anywhere on the app/website.

Common fields for both users/owners:

  • Full name
  • Display name
  • Phone number
  • Email and phone confirmation
  • Photo
  • Invoices: country, city, postcode, address

Simple users have these extra "individual" fields:

  • Gender
  • Nationality
  • Date of birth

Then, property owners don't have to specify gender/nationality/birth date, but they have only one "special" extra field: description about themselves.

Yeah, it's pretty simple. Property owners have only one personal field, everything else is related to their properties, which we will cover in the upcoming lessons.

So, let's try to think: all the common fields should probably be in the users DB table, just some of them nullable?

Ok, let's try to create that. First, we clearly need the migration for the countries table:

php artisan make:model Country -m

Migration:

Schema::create('countries', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
php artisan make:migration add_profile_fields_to_users_table
Schema::table('users', function (Blueprint $table) {
$table->string('display_name')->nullable();
$table->string('phone_number')->nullable();
$table->timestamp('phone_verified_at')->nullable();
$table->string('photo')->nullable();
$table->string('invoice_address')->nullable();
$table->string('invoice_postcode')->nullable();
$table->string('invoice_city')->nullable();
$table->foreignId('invoice_country_id')->nullable()
->constrained('countries');
});

But, hmm, isn't the users table becoming too big, then? Should ALL of those fields be here? How often would we actually use them to query the data?

And this is where we come back to the word "simulation" again. Let's simulate what queries would there be, and what fields would actually be selected?

For viewing a user's profile somewhere, we may need display_name and photo - those should be pretty common.

But what about the data for the invoices? Isn't it used ONLY for invoices? And how often do we generate the invoices, once a month? Or maybe even less than that, if the user is not actively paying for anything.

So we can leave the invoice fields in the users table, but then we're risking that some other developer would blindly do User::all() or a similar statement, without specifying the fields, and then it would unnecessarily download all those fields into memory.

So, what I would suggest is to create a separate DB table "user_profiles" with more rarely used fields. For now, we will put only invoice-related fields there but later may add more.

So, if we change the migration above, my suggestion is this:

php artisan make:model UserProfile -m

app/Models/User.php:

class User extends Authenticatable
{
public function profile()
{
return $this->hasOne(UserProfile::class);
}
}

Migration for Users:

Schema::table('users', function (Blueprint $table) {
$table->string('display_name')->nullable();
$table->string('phone_number')->nullable();
$table->timestamp('phone_verified_at')->nullable();
$table->string('photo')->nullable();
});

Migration for User Profiles:

Schema::create('user_profiles', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->string('invoice_address')->nullable();
$table->string('invoice_postcode')->nullable();
$table->string('invoice_city')->nullable();
$table->foreignId('invoice_country_id')->nullable()->constrained('countries');
$table->timestamps();
});

Now, the User::all() in the code would return only the main User fields, and if someone wants to know the invoice details, they would do User::with('profile')->get().

Another open discussion could be around the phone and phone_verified_at fields: should they be in users or user_profiles? It depends on how you actually use them in the application. For Booking.com, they are often used as an authentication/verification mechanism, so pretty important as a user field. But if in your app you use them only sometimes for displaying them in the profile, then I would move them into the profile.

Finally, you may want to ask why I called the DB table user_profiles instead of something like user_invoice_details. To answer that, let's talk about extra individual fields.


Fields Per Role: Where To Store Them?

The next question is about those extra fields, different for every role:

  • gender (User)
  • nationality (User)
  • birth_date (User)
  • description (Property Owner)

Quite often, I see people want to create separate DB tables for every role, like "doctors" and "patients", like "teachers" and "students", because their profile fields are very different.

I would give two pieces of advice/opinion about this:

  1. A little step-back, but: you should almost never replace the users table with multiple tables for authentication, repeating email/password in each of them. While it's technically possible, with Guards, it becomes a huge headache to later maintain the same functionality in more than one place. Any doctor/patient/teacher is a User of the system, first and foremost, and individual profiles are implemented in other DB tables, with a relationship to the "users" table.

  2. It makes sense to separate the profile tables only if they have many fields (10+), with most of them different per role. Otherwise, for a few fields, it's totally fine to use the same table, making some fields nullable.

With that in mind, for our specific case, it makes sense to just add those fields into the same user_profiles DB table, just make them nullable.

The full migration for the user_profiles would look like this:

Schema::create('user_profiles', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->string('invoice_address')->nullable();
$table->string('invoice_postcode')->nullable();
$table->string('invoice_city')->nullable();
$table->foreignId('invoice_country_id')->nullable()->constrained('countries');
$table->string('gender')->nullable();
$table->date('birth_date')->nullable();
$table->foreignId('nationality_country_id')->nullable()->constrained('countries');
$table->text('description')->nullable();
$table->timestamps();
});

If we put it visually:

User profile DB schema

For this feature, I don't think it's worth creating API endpoints and tests, as it's just a simple DB table to query.

Instead, I want to dedicate some extra time to...


Doctors and Patients?

While writing this lesson, I thought it is too simple: just one profile DB table. So decided to go a bit "off script" of this particular Booking.com project and discuss other, more complex, scenarios.

After all, the goal of this course is to discuss alternatives and explain the way to think about solutions, instead of just going "waterfall" with creating the project.

So, let's try to simulate what we would do for a typical doctor appointment system.

Again, first and foremost, both doctors and patients are users of the system, so we don't change anything in the users DB table, except for maybe adding more common and often used fields like phone_number.

Then, there will be two profiles with a lot of extra fields: some will be the same, and some will be different for patients and doctors.

Patients:

  • birth_date
  • gender
  • photo
  • social_security_number
  • emergency_contact_name
  • emergency_contact_phone_number
  • blood_group
  • address_street
  • address_city
  • address_country (for simplicity, let's assume it's a string instead of foreign key)

Doctors:

  • birth_date
  • gender
  • photo
  • doctor_id_number (in some national system)
  • description
  • speciality_id *(foreign key to specialities)
  • experience
  • education
  • clinic_id (foreign key to clinics)

There may be more fields, but let's stop at those, you get the picture: 10+ fields for each of the roles.

In this case, we obviously shouldn't make one user_profile table, for two reasons:

  • Too many unused fields
  • Those will be queried on the different pages and API endpoints, so often patient data will be queried without the need for any doctor's record

So, what I would suggest:

php artisan make:model Patient -m
php artisan make:model Doctor -m

Migrations:

Schema::create('patients', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->date('birth_date')->nullable();
$table->string('gender')->nullable();
$table->string('photo')->nullable();
$table->string('social_security_number')->nullable();
$table->string('emergency_contact_name')->nullable();
$table->string('emergency_contact_phone_number')->nullable();
$table->string('blood_group')->nullable();
$table->string('address_street')->nullable();
$table->string('address_city')->nullable();
$table->string('address_country')->nullable();
$table->timestamps();
});
 
Schema::create('doctors', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained();
$table->date('birth_date')->nullable();
$table->string('gender')->nullable();
$table->string('photo')->nullable();
$table->string('doctor_id_number')->nullable();
$table->string('description')->nullable();
$table->unsignedBigInteger('speciality_id')->nullable();
$table->text('experience')->nullable();
$table->text('education')->nullable();
$table->unsignedBigInteger('clinic_id')->nullable();
$table->timestamps();
});

Visually:

User doctors patients DB schema

You may ask, why not separate a table for repeating profile fields like birth_date / gender / photo?

Those ones are debatable and personal preference, you may even save them in the users table if you use them often. I don't see anything wrong if the fields with the same names are repeated in a few tables if they naturally belong there.

Final thought: I would probably suggest the same DB schema for similar situations like teachers/students, employees/employers, and others. I mean, have one users DB table and separate profile tables for each of the roles.

avatar

Great explanation  👍

avatar

great explanation, just one question about Profiles in 2 separate tables how to implemement a relationship between user and a profile?

avatar

In User model: $this->hasOne(Profile::class)

avatar

Thanks for the answer but i was not explain correctly. In the case you have 2 type of profiles, teachers/students for example with differents fields. I was think to a polymorph relationship but how to don't have a repetitive behavior with Roles?

avatar

It depends on what operations you want to have with those roles. It may be ok to have hasMany/belongsTo and call $user->teacher->fieldXYZ but if you want to have $user->profile->fieldXYZ then yes, probably polymorphic.

avatar

Hi Povillas,

i hope you are doing well and that you were spent good time during your holidays.

It could be interresting if you could add in the documentation the Model example for beginners devlopper. For User,Doctor and Patients like you did for the hasone relationship.

best regards

avatar

It's the same thing: User -> hasOne -> Doctor, and User -> hasOne -> Patient. Or maybe I misunderstood the question.

avatar

Very brilliant, practical and an indepth guide. This really covered many edge cases. I may consider becoming a premium member soon.

To discuss more on the last type of example i.e. users/doctors/patients, let's say we have users, owners and tenants and a user can be both an owner and a tenant.

  1. How will the relations look like? a) properties and bookings with users OR b) properties with owners and bookings with tenants

  2. While creating a new user record do we need to mandatorily have to create its respective owner and tenant record as well?

  3. To further extend this example, how would we design and manage multiple co-owner of a property? Like there will be one primary owner and secondary co-owners for different properties.

Now this may be more complex and out of scope for this guide, and may be too much to ask for, but if you can manage to take out the time to think and jot down some points, then I would be really grateful and interested to discuss further on how would we design such a system.

avatar

Personally, I would AVOID such situation where user can be both owner and tenant, that makes things hugely complicated. In many systems I know, if I want to have different type of account, I need to register with different email address.

avatar

Yes, I agree, it's quite challenging, but a client of mine wants something like that. The inspiration is taken from Airbnb, where the user can be both owner and tenant.

avatar

Maybe at some point I should spend time and make a demo project for this scenario, as there's no short answer at all. But it's such an edge case scenario, in my opinion, that I'm not sure a lot of audience will appreciate it. I'm trying to pick topics to help as broad audience as possible.

avatar

Sure, I can completely understand. May be once I implement this kind of system, I will document my approach, and send it to you, so that you can have atleast have something to start with, to further make a video/blog/project on this topic.

Better altogether would be, if you can create a series of re-creating Airbnb just like how you are doing it for booking.com now. And I think you can achieve this in comparatively less time, as many design decisions, logic and APIs of booking.com and airbnb.com would be overlapping and same I believe.

avatar

I remember seeing a lot of posts/videos/courses online labeled "Creating AirBnb clone", would you find the decision in any of those, maybe? For now, I'm focused on other topics and have 0 free time for upcoming weeks.

avatar

Oh no lol, most of them are very high level generic clone with no depth at all and uses services like auth0, firebase instead of production level tech-stack, which is understandable as they are more focused on beginners.

There is no one I have found yet, who goes into depth of topics like you, who makes complex topics and design decisions very easy to understand and comprehensible for mid level developers like me. I really appreciate that!

And no problem, Povilas. I just gave you an idea, you can take your time to come up with something regarding this in near future, if you see that it will provide enough value for a larger audience of yours. :)

avatar

Good night Povilas A while ago you had launched a discount coupon for matching the dollar x real. Could it regenerate something? Thank you very much

avatar

I have discounts here now: coupons.laraveldaily.com

avatar

Thank you very much Povilas

avatar

I love this !

avatar

Hi, What if users have more than 1 invoices. Should we have seperate invoice table? What you suggest!

avatar

Yes, of course then separate invoice table.

avatar

Best explaination!

I been looking for answers if I should split user profile with user table or combined it.

And if I should make profile for different roles or combined it.

You explained it well. Big thumbs up!

avatar

Hi,

It's about next lesson but I haven't a premium account so I ask it here if you don't mind.

For designing this structure: Country, City and Geographical Objects. Is it a good idea to make one table named Places with these columns:

id

name

type (country, city...)

country_id (nullable)

city_id (nullable)

And puting all of our data about our places in this table.

What are the problems that this design cause?

I know it is a bad idea, I just want to know why? I think it gives me a big picture and good understanding about database structure.

You are a great teacher. Thank you.

avatar

really great, thanks alot