2

Lets say that I have a shelf on which I can put items that are shelfable (not sure if that is actually a word but you can understand my point). So I have the following:

class Shelf
{
    /** @var ShelfableItemInterface[] **/
    private $items = [];

    // addItem, getItems...
}

interface ShelfableItemInterface
{
}

/** let's imagine that not all books are shelfable **/
class Notebook extends Book implements ShelfableItemInterface
{
}

class PhotoFrame implements ShelfableItemInterface
{
}

So those are the definitions, and now I can create shelfs with different items on them:

$shelf = new Shelf();
$shelf->addItem(new Notebook(...));
$shelf->addItem(new PhotoFrame(...));
$shelf->addItem(new PhotoFrame(...));

// will return the three items
var_dump($shelf->getItems());

So, this works perfectly while all that data is not persisted and retrieved from storage.

I'm currently using Doctrine ORM which can automatically fetch the related objects and instantiate the objects of their proper classes, but that only works if all the related items extend same class which should be set in the relationship definition.

This is a problem as I don't want to be forced to make the related items extend some class (as we can see the Notebook already extends something), but make the relationship work by interface.

I know that I still would have some mapping strategy (or however we should call that), so for example in the database we'll have a table with the following:

+----+---------+-----------+
| id | shef_id | item_type |
+----+---------+-----------+
|  1 |       5 |         1 |
|  2 |       5 |         2 |
|  3 |       5 |         2 |
+----+---------+-----------+

(additional data in tables per type)

So, I'm confused how to make this work or where to put the process, in order for the ORM (or my logic) to know that when I call getItems(), when fetching the related items, if "item_type" is 2, it should instantiate object from PhotoFrame, 1 for Notebook etc.

I don't know if I stated my problem clearly, but I hope you can understand me.

Update: I see that this is available in java - http://jpaobjects.sourceforge.net/m2-site/main/documentation/docbkx/html/user-guide/ch04s09.html without the class inheritance part, but I would like to learn how I can achieve this in php without such library

mysubject
  • 29
  • 3
  • The question here is what your goal is. Are you trying to implement the interfaces solely as a way to store the data? Or are you trying to find a way to store the relation that is already defined? For clarification: the question isn't unasnwerable, but it's unclear what your main goal is, as you can't do it the way you expected to do it. – Flater Aug 07 '18 at 12:20
  • my goal is to replicate the "in memory" example with persisted data. so when i fetch the shelf from database, it will know how to instantiate the items properly using the proper classes. – mysubject Aug 07 '18 at 12:22
  • shelvable i think – Ewan Aug 07 '18 at 12:40
  • is this question just about how to do this in your chosen ORM? because a type column is basically the answer – Ewan Aug 07 '18 at 12:44
  • @Ewan the ORM doesn't support this without making the related classes extend same base class, so my question is how can I do it without the orm – mysubject Aug 07 '18 at 12:49

1 Answers1

2

The issue here is that the database doesn't care about interfaces. Interfaces are a language/compiler construct and have no real meaning outside of the application. To an outside observer, it's impossible to see if two classes have the same properties because an interface demands it, or because of pure coincidence.

ORMs usually provide a basic handling of inheritance, but the database itself doesn't quite care about inheritance either. The database is interested in tables and keys, nothing more (I've oversimplified it, but the point still stands).

You seem to expect a solution in which you can implement an interface on a class and not have to change the data model to reflect that change. It doesn't work like that. If you want to store a "A is shelved on B" relationship, you will have to define a FK for every possible implementation of A. Or you do away with an FK constraint, which I don't quite suggest.


What you have here is a one-to-many relationship. A shelf can have many items, an item can only be on one shelf at a time.

One-to-many relationships dictate that the "many" each store an FK to the "one". Which in this case means that every ShelfableItem must have a ShelfId.

My PHP is a bit rusty. If I remember correctly, interfaces cannot implement properties/fields, only methods. I'm not sure how to do this in PHP syntax, but purely to show a basic example, I'll use C#.

Have the interface include the FK:

public interface ShelfableItemInterface
{
    int ShelfId { get; set; }
}

Implement the interface so that the items actually have a ShelfId:

public class Book : ShelfableItemInterface
{
    public int Id { get; set; }

    //other fields

    public int ShelfId { get; set; }
}

public class Jar: ShelfableItemInterface
{
    public int Id { get; set; }

    //other fields

    public int ShelfId { get; set; }
}

Store this value in your existing database tables:

+-----------------------------+
|            BOOKS            |
+----+---------+--------------+
| id | shelfid | other fields |
+----+---------+--------------+
|  1 |       5 |              |
|  2 |       5 |              |
|  3 |       5 |              |
+----+---------+--------------+

+-----------------------------+
|            JARS             |
+----+---------+--------------+
| id | shelfid | other fields |
+----+---------+--------------+
|  1 |       5 |              |
|  2 |       5 |              |
|  3 |       5 |              |
+----+---------+--------------+

(additional data in tables per type)

I understand why you think you need a "shelfableitems" table, but in reality it's more a burden than it is a blessing.

It creates a lot of extra work:

  • You need to define item types.
  • You need to retrieve items from the correct table based on their item type, which is less-than-obvious to do in a single query.
  • You forgo the data integrity of having a FK constraint.
    • Therefore, you run the risk of the referenced item (book/jar) already having been deleted but a "ghost entry" remains in the ShelfableItems table.

ALternatively, you could create an intermediary table that does have (optional) FK constraints, but then you need to make a separate column for every possible FK:

+----------------------------------------------------+
|            ShelfableItems                          |
+----+---------+----------+---------+----------------+
| id | shelfid |  bookid  |  jarid  |  photoframeid  |
+----+---------+----------+---------+----------------+
|  1 |       5 |    1     |  NULL   |   NULL         |
|  2 |       5 |  NULL    |   6     |   NULL         |
|  3 |       5 |  NULL    |  NULL   |   15           |
+----+---------+----------+---------+----------------+

This creates a lot of wasted space. You have N columns, but you can guarantee that in every row, you will have (N-1) columns that are NULL.

But, more importantly, implementing this intermediary table is not necessary for data integrity purposes. Everything works perfectly fine without it, and it takes less effort to develop/maintain the data integrity.

Flater
  • 44,596
  • 8
  • 88
  • 122
  • Thank you for the answer. My concern is how to make the proper instances when calling, for example, `$shelf->getItems()` – mysubject Aug 07 '18 at 12:48
  • @mysubject: Using my solution, you'd need to have access to a list of entity types which are shelvable, so that you can basically get "all books where shelfid = 1" and "all jars where shelfid = 1", ... and then merge all the lists to get you `getItems()` result. This entails needing to store this information somewhere. In C#, you could _potentially_ use reflection to automate the behavior, but I would advise against it, as the complexity far outweighs the benefit of doing so. – Flater Aug 07 '18 at 12:50
  • Understandable, same for php. Another concern for me is that if I would like to allow for third party plugins to add their own implementations, that would require changing the logic in own code – mysubject Aug 07 '18 at 12:53
  • @mysubject: If you go with the intermediary table instead, you'll still be required to list all shelfable types somewhere (either as your `type id` or as your list of FK columns). In all cases, you need to list the shelfable item types somewhere. So I suggest keeping the database clean (not having the intermediary table) and simply define `getList()` as the aggregate of type-specific lists ("books where shelfid = 1" and so on) – Flater Aug 07 '18 at 12:54
  • @mysubject: I assume if a third party creates an extension (`Cat implements ShelfableItem`), that they will then store their item table (`Cats`) themselves? If they do, they they're inherently also able to have their table (`Cats`) contain the FK (`ShelfId`) to the source table (`Shelves`). I don't see a way to make it work with the intermediary tably unless you throw FK constraints out the door, which is something I would definitely advise against. – Flater Aug 07 '18 at 13:04