Thinking Through Features In Laravel: Note Sharing
In our case, I’ve installed a fresh Laravel app and we will explore the concept of building some features for a Note sharing API. Not a real-life project, though in future posts in this series, I plan on using real project examples, but this should do the trick.
For this example, I want to only focus on the “sharing” feature of the app. I’m not particularly concerned about the creation of notes or how the notes will be displayed at the moment. The main focus will be:
- A user can share their note with another user.
- The original owner of the note will remain the owner, so they can revoke access that note.
- If a user share’s a note with another user, the shared user will be able to view that note.
There are constraints I’d like to point out. I want the owner of a note to remain the owner. Eventually, they will be able to give permissions to their note shares. I originally thought of making a copy of note when it is shared, but I thought it would be better for the user to have more control over their notes.
Laravel ships with a migration for the users table, so I made two migrations for the two tables I’m creating for this feature. One table to hold our notes and another table to hold shared notes.
I have a one-to-many relationship with users and notes. A user can have many notes and a note belongs to a user.
User.php —User model
Note.php — Note model
At this point, I’m not really sure relationships are necessary for the
SharedNote model. I’m sure I’ll need one in the near future, but I just want a place to store records of shared notes and to reference who shared notes with who. For our example, I think I’ll end up using it to confirm that a user can view another user’s note if it is shared with them.
Moving on, we have an endpoint for sharing a note with another user in
This route will use our share method in our
NotesController. In that share method in our controller, it takes a
share_with value in our request body, which will be the user id of the person that note will be shared with. We look up the note based on the id wildcard in the our API endpoint, and then create a
SharedNote record based on the note details. Then return the shared note results via JSON in the response.
Since writing tests ensures our confidence with our codebase, let’s write a feature test and test our endpoint.
Given we have two users and note record owned by Alice. When we make a POST request to the share endpoint with bob’s id as the
share_with value in the request body. Then we should assert that the correct JSON is displayed within details and we should have a 200 OK response.
Just a note, I created a model factory for notes. Model factories are super useful for generating data on the fly. Especially useful when writing tests. You can read more about them in the Laravel documentation:Database Testing - Laravel - The PHP Framework For Web Artisans
We can now share a note with another user. Our test is running green and everything is fine and dandy. However, there are still improvements I would like to address. What if a user shares a note with the same user more than once? How will we provide that check? If an owner of a note revokes a share from user, how should we handle that? Should we just remove the record from the
Also, we still have to address how the target user will be able to view an owner’s note while blocking non-shared users from viewing that note. I don’t want to have to handle that logic in our controller.
Thinking through our check, we could create service class that has provides a method for our check then we can store our shared note if the check is valid. Then we can use the create function in our NotesController. The purpose of this is to have our methods be testable (perhaps in a unit test in this case) and so we don’t have our logic muddying up our controller.
In my opinion, I feel like the only responsibilities of the controller should be:
- Accepting a request.
- Handle that request.
- Return a proper response.
I usually tend to apply these rules, even early in development of a project, so I can keep things clean and testable. Can you put logic in the controller if the solution is dead simple? Sure! I still do that from time-to-time. Nothing wrong with that. Though, I try to realize the main purpose of the controller layer from the beginning.
Implementing the Service Class
Thinking through, I could have a single action class that handles the share note functionality. Maybe it’s called
ShareNote that lives inside a
services directory. Then, I can have a method that handles the check then use that method in a
ShareNote class, I figured we can start by creating a
hasBeenShared method that checks if a note has already been shared to the intended user.
Then, we can create a
share method that calls the
hasBeenShared method. If
hasBeenShared returns false, all is good and we can grab the owner’s note and store a
SharedNote record. If true, we return
Then I wrote a couple of unit tests for our
hasBeenShared method to ensure our confidence that this method is working as intended.
These tests both provide assertions for two obvious use cases: if a note has already been shared and if a note has not been shared with the intended user. We run and we get green! Neat!
Next, in our
NotesController, I injected our
ShareNote service into the constructor.
Then we can use our
share method like so.
Much cleaner and we have made our logic testable. I decided to provide a check, if our share method returns null, then we can return the appropriate JSON response with the correct HTTP status code.
Awesome, but what about testing our share method? Should we unit test that? We could, but I figured we can let the feature test we wrote earlier handle that for us. After implementing the share method into our controller, if we run all tests, they still run green. I didn’t think it would be necessary to provide a unit test for it; at least for the moment.
Viewing a Shared Note
For viewing a shared endpoint, I’m thinking this should be it’s own endpoint, separate from showing a single note.
For our check method, it is going to be very similar to our
hasBeenShared method. I decided to add this method into our
ShareNote class. It didn’t make much sense to create a new class just for this check.
Provided some unit tests for our method.
Now let’s implement it into our controller.
Wrote a feature test for this endpoint to make sure the endpoint is functioning as intended.
A Few Things to Consider
Neat! All is well and tests are passing! A few things to mention though. There’s starting to become some duplication in our tests. We can probably tackle this by using a
setUp method and set up our world before we run our tests. That way we can use our data for each test.
Also, we can probably write our own middleware to check if a note is viewable by another user. This would clean up our controller even more since we would have that check in our middleware instead.
Then we can pass our middleware into our route like so:
You can read more on middleware in Laravel here: Middleware - Laravel - The PHP Framework For Web Artisans
We talked more about high-level concepts in this post. I tend to like thinking through features like this and explore different ways we can achieve the same result. Furthermore, we can build better software by building the API we want; as a result, a better system for your users. I’d like to think your own codebase is an API for yourself and your team.
[*] - DHH is an acronym for David Heinemeier Hansson who is most notable for creating the web framework, Ruby on Rails.