As many of you know Laravel provides a concept for dealing with Polymorphism in Eloquent relationships called “Polymorphic Relationships“. It’s a nice abstraction, but if you are not careful it might lead to a lot of type checking at runtime all over the place.

There’s nothing wrong with the Polymorphic Relationships, you only have to keep in mind that whenever you need it, you have to think about behavior instead of types. Luckily for us, Object-Oriented Programming (OOP) has a concept that fits like a glove here: Polymorphism.

The naming isn’t just an accident. Let’s see the problem before we talk about the solution.

The Problem

Let’s assume we are building a really robust and abstract permission system. The idea is that we can create permissions so that users can interact with certain resources. A permission entry has a subject (the resource) and a consumer (the user).

users
  id - int
  name - string

resources
  id - int
  name - string
  external_id - string # represents the resource ID in an external service

permissions
  id - int
  subject_type - string
  subject_id - int
  consumer_type - string
  consumer_id - int

Good enough. If you pay attention to the permissions table, you will see that both consumer and subject concepts got 2 fields for the relationship: type and id.

The type one holds the reference to the Eloquent model class*, while the id one holds the primary key of that model.

* Note: by defailt, the Fully-Qualified Class Name (FQCN) will be stored in the database. That is sometimes a pain when you have to rename models or move classes around when refactoring. However, you can fully configured what is stored as the type using aliases that map to the actual FQCN, see here.

Your models should look something like this:

<?php

class User extends Model
{
  public function permissions()
  {
    return $this->morphMany(Permission::class, 'consumer');
  }
}

class Resource extends Model
{
  public function permissions()
  {
    return $this->morphMany(Permission::class, 'subject');
  }
}

class Permission extends Model
{
  public function subject()
  {
    return $this->morphTo();
  }

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

Cool. Here’s an example of these relationships in use:

$user = User::all();

foreach ($user->permissions as $permission) {
  // Should print out the resource 
  // associated with this permission.
  dump($permission->subject);
}

All fine so far. Now, let’s say that we need to export these permissions to an external service. Ok, simple enough, something like this should do the trick:

$cursor = Permission::query()->cursor();

foreach ($cursor as $permission) {
   Zttp::post($this->url(), [
     'resource_id' => $permission->subject->external_id,
     'user_id' => $permission->consumer->id,
   ]);
}

Uhm. At first sight, it might look fine. The $permission->subject returns the Resource associated with the Permission, while the $permission->consumer is the User that was granted that permission, right? Well, not so sure. Remember that we are using a polymorphic relationship here.

In other words, it might be that these are indeed the behavior of the permission, but it also might be something else completely. That’s why we are using a polymorphic relationship in the first place.

Let’s make it a bit more complex by introducing grouping.

Grouping Resources and Users

Now, we can group both Resources and Users. And you can grant these Permissions to them using these groups. However, the external service doesn’t have any of these “grouping” concepts. It only cares about Users and Resources, a flat map.

Since our Permission model is using polymorphic relationships, we can easily make a UserGroup a consumer and a ResourceGroup a subject.

Luckily for us, our external service accepts a list of IDs for both resources and users.

Our first code to export these permissions need some change, because now the $permission->subject and $permission->consumer no longer return only User and Resource instances, respectively. It can also return UserGroup and/or ResourceGroup, again, respectively.

Easy enough, you can do something like this:

$cursor = Permission::query()->cursor();

foreach ($cursor as $permission) {
   Zttp::post($this->url(), [
     'resource_id' => $permission->subject instanceof Resource 
       ? [$permission->subject->external_id] 
       : $permission->subject->resources()->pluck('external_id')->all(),
     'user_id' => $permission->consumer instanceof User
       ? [$permission->consumer->id]
       : $permission->consumer->users()->pluck('id')->all(),
   ]);
}

Uhm… it doesn’t look pretty, does it? Also, what happens if we add yet another model as Subject? Or another Consumer? Yeah, more type checks, not cool. You can split these up into smaller methods, but that’s just hiding complexity.

There’s another way.

Enters Polymorphism

As I mentioned in the beginning of the post, you can use polymorphism to solve this.

In programming languages and type theory, polymorphism is the provision of a single interface to entities of different types or the use of a single symbol to represent multiple different types.

Wikipedia

Reading the definition, that looks exactly like what we want. In fact, there was a hint in the relationship name: “Polymorphic Relationship”.

I’m not saying this is the only way of dealing with this. However, I found this approach really good and clean.

Whenever you need a polymorphic relationship, introduce an interface that will represent that behavior.

That’s it. In our case, we have two concepts: Subject and Consumer of permissions. All we care right now is a way to retrieve the IDs of these types. The interfaces would look like this:

namespace App\Permissions;

interface Subject
{
  public function getSubjectIdsForPermissions(): array;
}

interface Consumer
{
  public function getConsumerIdsForPermissions(): array;
}

Cool. We these in the same file here, but that’s just for demonstration purposes. Now, let’s implement these in our User and UserGroup models, which are consumer.

use App\Permissions\Consumer as PermissionsConsumer;

class User extends Model implements PermissionsConsumer
{
  public function getConsumerIdsForPermissions(): array
  {
    return [$this->id];
  }
}

class UserGroup extends Model implements PermissionsConsumer
{
  public function users()
  {
    return $this->belongsToMany(User::class);
  }

  public function getConsumerIdsForPermissions(): array
  {
    return $this->users()->pluck('id')->all();
  }
}

Looks good. Now, the Resource and ResourceGroup models are subjects of our permissions, so they could look like this:

use App\Permissions\Subject as PermissionsSubject;

class Resource extends Model implements PermissionsSubject
{
  public function getSubjectIdsForPermissions(): array
  {
    return [$this->external_id];
  }
}

class ResourceGroup extends Model implements PermissionsSubject
{
  public function resources()
  {
    return $this->belongsToMany(Resource::class);
  }

  public function getSubjectIdsForPermissions(): array
  {
    return $this->resources()->pluck('external_id')->all();
  }
}

Great. Now, our final implementation of the exporting would look like this:

$cursor = Permission::query()->cursor();

foreach ($cursor as $permission) {
   Zttp::post($this->url(), [
     'resource_id' => $permission->subject->getSubjectIdsForPermissions(),
     'user_id' => $permission->consumer->getConsumerIdsForPermissions(),
   ]);
}

That’s better, right? At least to me. You can even help your IDE with auto-completions on these relationships by adding docblocks in your Permission model, like:

/**
 * Attributes
 * 
 * @property \App\Permissions\Subject $subject
 * @property \App\Permissions\Consumer $consumer
 */
class Permission extends Model
{
  public function subject()
  {
    return $this->morphTo();
  }

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

Now, whenever you reach for $permission->subject or $permission->consumer, your IDE will show the available methods in those interfaces.

You can go even further and use @property-read instead, which will remind you in your IDE that you cannot override these properties externally. Then, you can have domain methods that are correctly type-hinted using the interfaces, like so:

use App\Permissions\Subject as PermissionsSubject;
use App\Permissions\Consumer as PermissionsConsumer;

/**
 * @property-read \App\Permissions\Subject $subject
 * @property-read \App\Permissions\Consumer $consumer
 */
class Permission extends Model
{
  public static function newPermission(
    PermissionsSubject $subject,
    PermissionsConsumer $consumer
  ): Permission {
    $permission = new static();

    $permission->subject()->associate($subject);
    $permission->consumer()->associate($consumer);

    return $permission;
  }

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

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

Conclusion

Although these examples were pretty simple (we are only using get operations in our interfaces), you can have more complex abstractions and do more fancy things with these interfaces. Each class that implements the interface will have its own behavior for these.

Anyways, hope this was fun to you. I have been thinking about this approach for a while over the past few days, because we are working with a couple Polymorphic relationships in one of our projects.

Let me know what you think.