5
\$\begingroup\$

This is an OOP question and probably answerable even if you don’t have PHP knowledge. (A side note – we are learning PHP OOP and decided to create a github to hopefully help others learn this and other best practices a bit easier, https://github.com/nizamani/PHPBestPractices1-OOP if you are interested. PRs, issues etc are very welcome)

I think we have two questions that are intertwined, but if you think I should split this up into a single question please let me know.

Overview

A simple app with 3 database tables, containing users’ name, age, favorite food and favorite restaurant.

Goal/Output

Our current goal (exact question will be listed below) is to use OOP and a user object to get and display a single user's name, age, favorite food, and favorite restaurant.

Example output:

"User's name is Jenn, age is 28, favorite restaurant is KFC and favorite food is Fried Chicken"

Database

To make this as simple as possible for beginners to try out, we’ve created a "fake db" using php arrays. The db “tables” are:

users
id    name     age    favoriteRestaurantId   favoriteFoodId
1     Mike     30     1                      1
2     Jenn     28     3                      3

restaurants
id    name
1     McDonalds
2     Taco Bell
3     KFC

foods
id    name
1     French Fries
2     Hamburger
3     Fried Chicken

Faked as php arrays:

// this represents a sql table named `users`
$users = array(
    array("id" => 1, "name" => "Mike", "age" => 30, "favoriteRestaurantId" => 1, "favoriteFoodId" => 1),
    array("id" => 2, "name" => "Jenn", "age" => 28, "favoriteRestaurantId" => 3, "favoriteFoodId" => 3)
);

// this represents a sql table named `restaurants`
$resturants = array(
    array("id" => 1, "name" => "McDonalds"),
    array("id" => 2, "name" => "Taco Bell"),
    array("id" => 3, "name" => "KFC")
);

// this represents a sql table named `foods`
$foods = array(
    array("id" => 1, "name" => "French Fries"),
    array("id" => 2, "name" => "Hamburger"),
    array("id" => 3, "name" => "Fried Chicken")
);

(https://github.com/nizamani/PHPBestPractices1-OOP/blob/master/src/fakedatabase/db.php)

Classes

Three classes, one for each table. Each column (except id) in the db maps to a property of the object.

class User
{
    private $name;
    private $age;
    private $favoriteRestaurantId;
    private $favoriteFoodId;

    //...setter and getter methods, nothing more
}

class Restaurant
{
    private $name;

    //...setter and getter methods, nothing more
}

class Food
{
    private $name;

    //...setter and getter methods, nothing more
}

(https://github.com/nizamani/PHPBestPractices1-OOP/blob/master/src/classes/Domain/User/User.php

https://github.com/nizamani/PHPBestPractices1-OOP/blob/master/src/classes/Domain/Restaurant/Restaurant.php

https://github.com/nizamani/PHPBestPractices1-OOP/blob/master/src/classes/Domain/Food/Food.php)

Current Usage

On a simple page, we want to display user 2's info. Their name, age, name of their favorite food and name of their favorite restaurant. This is how we do that, which we think is way too "manual" of a process:

    // create user object
    $userObject = UserFactory::createUser();
    // get user data from the db and set to User object
    $userRow = $this->usersTransactions->getUserById(2);
    $userObject->setName($userRow["userRow"]["name"]);
    $userObject->setAge($userRow["userRow"]["age"]);
    $userObject->setFavoriteRestaurantId($userRow["userRow"]["favoriteRestaurantId"]);
    $userObject->setFavoriteFoodId($userRow["userRow"]["favoriteFoodId"]);

    // create restaurant object
    $restaurantObject = RestaurantFactory::createResturant();
    // get restaurant data from the db and set to Restaurant object
    $userfavoriteRestaurantIdRow = $this->restaurantsTransactions->getRestaurantById($userObject->getFavoriteResturantId());
    $restaurantObject->setName($userfavoriteRestaurantIdRow["restaurantRow"]["name"]);

    // create food object
    $foodObject = FoodFactory::createFood();
    // get food data from the db and set to Food object
    $userfavoriteFoodIdRow = $this->foodsTransactions->getFoodById($userObject->getFavoriteFoodId());
    $foodObject->setName($userfavoriteFoodIdRow["foodRow"]["name"]);

    // this will display the user's information
    $this->response->setView('displayUserInformation/index.php');
    $this->response->setVars(
        array(
            "name" => $userObject->getName(),
            "age" => $userObject->getAge(),
            "restaurant" => $restaurantObject->getName(),
            "food" => $foodObject->getName()
        )
    );

(https://github.com/nizamani/PHPBestPractices1-OOP/blob/master/src/classes/Controller/DisplayUserInformationPage.php)

Questions

Question #1 Would you agree that modifying the user object to contain favorite restaurant name and favorite food name would be best? That is the primary information we are going to be concerned with displaying throughout the app, so we think it logical:

class User
{
    private $name;
    private $age;
    private $favoriteRestaurantId;
    private $favoriteFoodId;
    private $favoriteRestaurantName;
    private $favoriteFoodName;

    //...setter and getter methods, nothing more
}

Question #2 Currently, anywhere we want a user's info, we have to do a lot manually to create a user object, food object, and restaurant object. Seems ripe for mistakes. Imagining a new page where we want to display info of all of the users in our db, we will loop through user ids and then have all this manual stuff inside that loop. That doesn't seem logical, doesn't seem DRY. We think something like this would be very useful to, for example, get user id 2's info:

$user = createUserObjectByUserId(2);

Then on our hypothetical page where we want to display every user's info, we simply loop through user ids and call this createUserObjectById method. Would you agree? We don't know how best to do this. In our codebase we have a UsersTransactions class, but it is concerned with getting info from the users table. What is the best practice here? We want to follow dependency injection best practices and OOP best practices, and can't really come up with a good way to accomplish this.

--

Thank you for taking the time to read all of that, and please feel free to tell us everything you would do differently here and in our github if you want to take a look at that. We want to learn best practices and get this right, and hopefully have a decent example for others to learn from.

\$\endgroup\$
3
\$\begingroup\$

Question 1

I would agree with this statement, and would actually take it further to follow "proper OOP practices". If you choose to implement your classes this way, you do not need to store the IDs of other classes - you can just store a variable that references the other class directly. So instead of $favoriteRestaurantId and $favoriteRestaurantName, you can just have $favoriteRestaurant and then access all the relevant properties of Restaurant through that variable i.e $favoriteRestaurant->getName().

Further explanation

Right now we have two ways of modelling data - in the database, and in our PHP objects. Both environments are concerned with the relationships between objects, but they implement these relationships differently. We want to use the solution that fits the language we are using at any given time.

In the database, we model relationships with unique identifiers. In your case, these are unique integers for each row in a given table. So in the context of the database, if I want to know the name of Mike's favorite restaurant, I would first find the user with name "mike", find his matching favoriteRestaurantId, then find the matching restaurant with that id and finally retrieve the name of that restaurant. In this scenario, the id of the restaurant is acting as a handle to a restaurant object. We can store the handle anywhere in the database, and retrieve information about the object the handle represents by finding the corresponding row in the restaurant table.

In PHP, variables act as handles to other objects automatically. For example, if I have a class User with a variable favoriteRestaurant, then I can do something like

$mike = UserFactory::createUser();
$mcDonalds = RestaurantFactory::createRestaurant();
$mike->favoriteRestaurant = $mcDonalds;

In the database, we represented this relationship between objects by getting a handle to the McDonald's restaurant (the restaurant ID 3) and storing it somewhere associated with Mike (the field favoriteRestaurantId). Similarly, in PHP, we can represent this relationship between objects by getting a handle to the McDonald's restaurant (the variable $mcDonalds) and storing it somewhere associated with Mike ($mike->favoriteRestaurant).

The benefit of using PHP's built-in mechanisms for representing relationships, is that we can more easily find information about these relationships. Consider the case where we want to store more information about the restaurants, such as hours of operation. Previously, we would have had to either store this information directly in the User object as another variable ($favoriteRestaurantHoursOfOperation), or look up the restaurant object by ID every time we wanted to know this information. By storing the handle to the user's favorite restaurant directly as a variable, we can add the relevent information in the Restaurant class (where it belongs) and access it by calling $mike->favoriteRestaurant->getHoursOfOperation();. This represents the relationship between these objects more visibly.

Question 2

This is also a good idea, and in fact is exactly what the Factory pattern is designed to solve. You can do this fairly easily by just moving around some code that you have already written. I will show an example for the UserFactory:

class UserFactory
{
    public static function createUser($userId)
    {
        $userObject = new User();
        // get user data from the db and set to User object
        $userRow = $usersTransactions->getUserById($userId);
        $userObject->setName($userRow["userRow"]["name"]);
        $userObject->setAge($userRow["userRow"]["age"]);
        $userObject->setFavoriteRestaurantId($userRow["userRow"]["favoriteRestaurantId"]);
        $userObject->setFavoriteFoodId($userRow["userRow"]["favoriteFoodId"]);
        return $userObject;
    }
}

You'll need a way to use the UserTransactions class inside the UserFactory, and there are several ways to accomplish this - pass it in via constructor and create a new UserFactory each time you want a user, construct a UserTransactions instance each time you call createUser, have a globally-accessible UserTransactions instance, etc. I haven't looked at enough of your code to know how you handle this, so find the option that works best for you.

Once you have implemented the UserFactory as above, you can use it elsewhere by simply calling $userObject = UserFactory::createUser(2); (using 2 from your given example).

You can also combine this with the solution in Question 1 by changing the following two lines

$userObject->setFavoriteRestaurantId($userRow["userRow"]["favoriteRestaurantId"]);
$userObject->setFavoriteFoodId($userRow["userRow"]["favoriteFoodId"]);

to something like

$userFavoriteRestaurantId = $userRow["userRow"]["favoriteRestaurantId"];
$userObject->setFavoriteRestaurant(RestaurantFactory::createRestaurant($userFavoriteRestaurantId));

$userFavoriteFoodId = $userRow["userRow"]["favoriteFoodId"];
$userObject->setFavoriteFood(FoodFactory::createFood($userFavoriteFoodId));

Other Notes

  • Not sure how your $userRow is implemented, but it would make more sense to be able to type $userRow["favoriteFoodId"] rather than having to add the redundant ["userRow"];
  • If your restaurant and food objects really do only contain a name, then it might be fine to simply store $favoriteRestaurantName and $favoriteFoodName in the User object, and completely remove the Restaurant and Food objects. However, if you want to properly model the relationships between objects, it's recommended to go with the solution to Question 1 and store this info as handles to the actual Restaurant and Food objects respectively.
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.