01
Jun '18

I’m sure we’ve all been there. You create a nice data structure for a utility and the client says “Ok, but I also need to be able to add a note to it”. You add a note field and then they say “Ok, but I also need to add a note over here”. You add another note field over there and then they say “But I need to be able to add new notes and keep the old ones”. You scream internally, wish they’d told you that in the first place, and start building a more flexible solution.

Well here is my take on it. It’s a separate notes table with a polymorphic association to any and all other records. Their models simply need to use this ‘Notable’ trait. The notes will be created automatically with any save action on the parent model, and have a simple functions to retrieve all notes against a parent, or clear them. Simples.

This assumes that you have either a single ‘note’ parameter in your request, or a ‘notes’ array, and have triggered a save on the parent record.

Table structure:

CREATE TABLE `notes` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `parent_id` int(11) NOT NULL,
  `parent_type` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `content` text COLLATE utf8_unicode_ci NOT NULL,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`)
);

Trait:

namespace App\Trt;

/**
 * Notable trait
 */
trait Notable
{
    /**
     * Custom boot method
     */
    public static function bootNotable()
    {
        /*
         * Trigger on create
         */
        static::saved(function ($record) {
            //We check the class name here as otherwise it will get in an infinite loop of trying to add notes below notes
            if(class_basename(get_class($record)) != 'Note' && (request()->note || request()->notes)) {

                if(request()->note) {
                    $record->notes()->create(['content' => request()->note]);
                }

                if(request()->notes && is_array(request()->notes)) {
                    foreach(request()->notes as $note) {
                        $record->notes()->create(['content' => $note]);
                    }
                }
            }
        });

        /*
         * Trigger on delete
         */
        static::deleting(function ($record) {
            $record->clearNotes();
        });
    }

    /**
     * Note relationship
     */
    public function notes()
    {
        return $this->morphMany('\App\Model\Note', 'parent');
    }

    /**
     * Clear all notes
     */
    public function clearNotes() {
        $this->notes()->delete();
    }
}

I tend to include the ID of the current user in the note too, so that I can display the user name against the note text. Simple case of adding the column to the table and the fillable array, adding the relationship function, and replacing:

$record->notes()->create(['content' => request()->note]);

with:

$record->notes()->create(['content' => request()->note, 'user_id' => \Auth::user()->id]);