10

Is there a design pattern for building a GitHub-inspired timeline? I'm trying to write a somewhat complex and versatile timeline system for my application. It is based around this concept:

[Subject] [Verb] [DirectComplement] [IndirectComplement] (metadata: [date])

So, in practice:

John created a new post called Beautiful Post (12/01 00:01)

John is the subject, created is the verb, Beautiful Post is the direct complement.

John commented "OMG" on Beautiful Post  (12/01 00:00)

John is the subject, commented is the verb, "OMG" is the direct complement and Beautiful Post is the indirect complement.

I'm working with Symfony2 and Doctrine, running MySQL. I have created an entity named Timeline which stores, as string, the model of the Subject, DirectComplement and IndirectComplement, as well their IDs. Then, manually, i make the proper queries in order to fetch the objects of each one.

Is there a proper way of doing this with Doctrine and MySQL? A more elegant and versatile approach which does not make me crazy and forcing me to make an absurd amount of queries and foreachs?

vinnylinux
  • 7,050
  • 13
  • 61
  • 127

1 Answers1

36

About database schema

ActivityStrea.ms is a standard proposal for social activity streams like the one you want. There's a lot of similar posts here on SO mostly regarding database design of these activity streams (links at the end). Please, don't underestimate the reading of ActivityStrea.ms JSON Schema. I'm sure you will learn a lot from it.

I would suggest you to use this database design:

user_id        |  INTEGER  |  user being notified
actor_id       |  INTEGER  |  user performing the action
activity_type  |  STRING   |  classname/type of the object being notified
activity_id    |  INTEGER  |  id of the object being notified
context_type   |  STRING   |  classname/type of the object's parent
context_id     |  INTEGER  |  id of the object's parent
read/view_at   |  DATETIME |  timestamp of when user saw it

So, for instance, if someone comments a post you would have something like:

$notification = array(
    'user_id'       => $id_of_the_user_being_notified
    'actor_id'      => $comment->user_id
    'activity_type' => 'comment'
    'activity_id'   => $comment->id
    'context_type'  => 'post'
    'context_id'    => $comment->post_id
    'read_at'       => NULL
);

It seems unnecessary to have all those fields, but they will surely pay their cost.

With that design you can:

  1. Group related notifications by user, type or context
  2. Filter notifications by action type, context type and specific actors (through join)
  3. Easily purge notifications from objects that are deleted (suppose an user deletes a post that was commented. Notifications of its comments should disappear)

Note: Timestamps (created_at/updated_at) aren't really necessary. Since you will load the activity object (the comment record in that case), you will have its timestamps already. The only reason to have them duplicated is to query "notifications" by timestamps (you won't be able to use JOIN here). Anyway, feel free to add them as you see fit.

About Doctrine and Symfony

I can't say much for Symfony, but I'm sure Doctrine supports polymorphic queries. With that in mind, it should play nicely with that design.

On Laravel we use the approach of models implementing a NotifiableInterface. That way, different models can have their own logic of who gets notified by it and which is its context.

The application itself listen to model's create method and generates notifications when fit, so models and controllers don't have to deal with notifications themselves, are nicely decoupled and changing storage should be as easy as possible.

Example of NotifiableInterface

This is a pretty simple example of a NotifiableInterface. You should use it as inspiration and adapt it according to your needs.

interface NotifiableInterface {

    // Returns a string representation of the type
    // It may be a get_class($this), the model table
    // or anything you like really.          
    public function get_type();

    // Returns an identifier for the object (ie its ID)
    // get_key is for compatibility with Laravel models
    // but you may use get_id.
    public function get_key();

    // Returns the context object for this entity.
    // It's advisable that this object also implements
    // NotifiableInterface, so it also has get_type and get_key
    public function get_context();

    // Returns the user_ids to be notified.
    // A comment, for instance, should notify the post owner
    // as well as anyone else that commented that post.
    public function should_notify();

}

Related Questions

Here are a few posts with plentiful information on that topic:

Community
  • 1
  • 1
vFragosop
  • 5,705
  • 1
  • 29
  • 31
  • @vFragosop, This is after 2 years of your answer, hope you are doing well :) How do I record an activity if the comment is removed ? or if someone unlike what they liked before ? Any thoughts on this please ? – Sahan Jul 13 '15 at 09:11
  • @Sahan, there's three possible ways: **1.** You can override the model `delete` method and remove all notifications for that combination of type and id. **2.** You can hook on model's delete event and do it there. **3.** You can create a service class like `CommentingService` that manages creation and deletion of everything. So instead of doing `$comment->delete()` you do `$commentingService->deleteById($id)` or something like that. The later is a lot better as a long term solution because is easier to inject dependencies, test and refactor. – vFragosop Jul 14 '15 at 18:22
  • @vFragosop - One problem with the solution is that it doesn't keep track of the verb. - "Created, Answered, Completed" etc. Might have to add one more column to it. – Abijeet Patro Apr 29 '16 at 09:21
  • @AbijeetPatro we internally use the `activity_type` as a suggestion of the verb.You could store the verb in plain english inside of your database, but that's not a good approach in terms of i18n. – vFragosop May 02 '16 at 14:45