2

I recently took over a project that has an existing user system in place (standard Laravel 5.7 make:auth setup). What I've come to identify is that there is a significant amount of MySQL database columns within the user table that are blank or NULL as a result of the varied user types within the system. For the purpose of this example, let's say they are ADMINISTRATOR, PARENTS, CHILDREN. Within the app currently though, there are no clear distinctions as to what a user is other than a column called "type" which is either NULL for children, 'admin' for ADMINISTRATOR, and 'parent' for PARENTS.

I want to be able to seperate all these columns out to be associated with the appropriate user type while maintining the same authentication method (that is my current thought process, not set in stone).

From what I've gathered, I should be looking at a polymorphic relationship in which the User model has a "typeable" morph relationship to each of the user types. I've also read that that entails having a "typeable_type" and "typeable_id" column on the user model where "type" refers to the model - in this case "App\Models\Admin", "App\Models\Parent", "App\Model\Child".

For each of these I would do something like this:

class User extends Model
{

    public function typeable()
    {
        return $this->morphTo();
    }
}

class Admin extends Model
{
    public function user()
    {
        return $this->morphMany('App\User', 'typeable');
    }
}

class Parent extends Model
{
    public function user()
    {
        return $this->morphMany('App\User', 'typeable');
    }
}

class Child extends Model
{
    public function user()
    {
        return $this->morphMany('App\User', 'typeable');
    }
}

My question is - how can I create relationships within the "type" classes that can use the parent User ID as the foreign key / basis of any relationship?

For instance, a Parent might have a subscription in the database that perhaps a Admin doesn't. The subscription table references the current user via a "user_id" column.

Traditionally, the User would just reference the subscription through a normal relationship.

class User extends Model
{

    public function subscription()
    {
        return $this->hasOne(App\Models\Subscription::class);
    }
}

class User extends Model
{

    public function subscription()
    {
        return $this->hasMany(App\Models\User::class);
    }
}

How would I retrieve that in an architecture where that relationship is owned by the "Parent" class. Take in mind, I'm working within a legacy app that I inherited, so everything currently revolves around the user & user_id.

Long story short, I want to be able to pull different types of info depending on user type but still treat each of the user types as a "User" in the traditional sense. An ideal system would be where the information that each user type possesses is "flattened" into the standard user for use.

radiantstatic
  • 342
  • 4
  • 20

1 Answers1

2

I don't think using a relationship is what you're really after here.

The way I would set it up is the create a User class (that extends Model) and then have three models extend the User class (Parent, Child and Admin).

In the User base class you would add all code that applies to all users.

I would then add a global scope to each of the Parent, Child and Admin classes (for the example, I've used an anonymous global scope, but you can use a normal scope if you wish):

class Admin extends Users
{
    protected static function boot()
    {
        parent::boot();

        static::addGlobalScope('user_type', function (Builder $builder) {
            $builder->where('type', '=', 'admin');
        });
    }
}

You would then add any relevant methods into each of the three classes, for example, your subscription relationship would look like this:

class Admin extends User
{
    public function subscription()
    {
        return $this->hasOne(Subscription::class);
    }
}

class Subscription extends Model
{
    public function admins()
    {
        return $this->hasMany(Admin::class);
    }
}

You then might also need to set the table in each of the three classes by using protected $table = 'users' - but I'm not certain on this.

If you were then provided just a user class and wanted to convert that user into one of the specific base classes, you could add a function like this to the class:

class User extends Model
{
    public function getUserType() // Or whatever you would like the name to be
    {
        // NOTE: I've made this function load without having to do any database calls
        // but if you don't mind an additional call, it might be easier to just change
        // the switch to return Admin::find($this->id);

        $class = null;
        switch($this->type) {
            case "admin":
                $class = new Admin($this->attributesToArray());
                break;
            case "parent":
                $class = new Parent($this->attributesToArray());
                break;
            case "child":
                $class = new Child($this->attributesToArray());
                break;
        }

        if(is_null($class)) {
            // Throw error, return null, whatever you like
            return $class;
        }

        // Perform any additional initialisation here, like loading relationships
        // if required or adding anything that isn't an attribute

        return $class;
    }
}
Nick Clark
  • 141
  • 2
  • I appreciate the time spent in your response, this sounds like a legitimate path I should consider and honestly something that could be migrated relatively quickly and piecemeal. Figuratively, I’m just looking to extend a User into different types and this is a tangible implementation. I’d ideally like to hear if there are any other paradigms around this concept - specific to Laravel - but overall this is a solid solution. With this, would you simply just have tables specific to user types that just reference them in as standard users through user_id? An admin_meta table, parent_meta table... – radiantstatic Aug 18 '19 at 05:21
  • 1
    Read this if you want to know the pros and cons of each way of storing the data https://stackoverflow.com/questions/3579079/how-can-you-represent-inheritance-in-a-database – Wouter Van Damme Aug 20 '19 at 14:35
  • 1
    @radiantstatic I don't believe there is any Laravel specific paradigms, the only thing that would be specifically Laravel is [Gates and Policies](https://laravel.com/docs/5.8/authorization). I'd personally steer away from meta tables as they will likely lead to data duplication. If you want something Laravel specific I would look into Policies. Normally (if it wasn't Laravel) I would recommend implementing a `role` table (`id`, `name`), a `permissions` table (`id`, `permission`) and a `role_permissions` table (`role_id`, `permission_id`) and using the role to check whether a permission exists. – Nick Clark Aug 23 '19 at 21:47
  • @NickClark - so (for the time being) I'm truly concerned less about the permissions aspect of it - although I understand its importance. For examples sake, assume that most everyone can do everything minus alter the data not applicable to them. In Wouter Van Damme's response - I see the "Class Table Inheritance" as the most applicable scenario. Obviously its semantics as to calling it meta - but would you still disagree with that pattern? – radiantstatic Aug 26 '19 at 15:48
  • @radiantstatic There isn't really an incorrect solution to the problem and it just depends upon if you're going to have customisable permissions in the future (or currently) and where you want the permissions to live (either in the database or in the application code). As I mentioned previously, the only real issue I see with using meta tables is the potential for data duplication in the database - but it's going to be a really minor thing so if you feel most comfortable with meta tables, then use them – Nick Clark Aug 31 '19 at 00:57