The task from iPhone Photography School.
These have been the assumptions while working on the test application.
-
User can have many achievements and vice versa.
-
User can have many badges and vice versa.
-
The events
Lesson Watched
andComment Written
are fired whenever a user watches a video or comments down, and the application acts according to that. -
The application has following achievements:
- First CommentWritten
- Three Comments Written
- FiveCommentsWritten
- TenCommentsWritten
- TwentyCommentsWritten
- FirstLessonWatched
- 5 Lessons Watched
- 25 Lessons Watched
- 50 Lessons Watched
-
The application has following badges:
- Beginner: 0 Achievements
- Intermediate: 4 Achievements
- Advanced: 8 Achievements
- Master: 10 Achievements
Steps I thought of and executed for solving the given problem:
-
Prepare database tables which can persist the assignment of badges and achievements for retrieval.
-
Hook the event-listeners of
CommentWritten
,LessonWatched
and handle the logic of assigning achievements of either comment written or lesson watched.- Add those achievements classes by their respective types either
lesson_watched
orcomment_written
within the app and a service providerAchievementServiceProvider
.Theboot
method for this provider loops over the classes and instantiates and keeps them inside a singleton class within the container. - Afterwards when a user comments or watches video the respective events fire and the listeners
AwardAchievementsForCommentsWritten
,AwardAchievementsForLessonWatched
handle method access the achievements from the container and determine the event with the help of a propertyachievementType
within the achievement classes and sends them to proper achievements to check whether the subject in question qualifies for the event or not. - If they qualify for the achievement add the record to the
user_achievements
table and fire eventAchievementUnlocked
as the task has asked.
- Add those achievements classes by their respective types either
-
Add badge classes in the
Badges
directory and allow them to determine badges.- Assign badge to the user in question when they receive an achievement with the help of the event
AchievementUnlocked
and listenerAssignBadges
. It also uses the exact approach of handling achievements, the only difference is the logic to ask the subject in question to award a badge. Please refer toBadgeServiceProvider
and the classes inside theBadge
directory to find more.
- Assign badge to the user in question when they receive an achievement with the help of the event
-
Add a console command to sync the achievements of existing users.
- The achievements have a type of
comment_written
andlesson_watched
and for the case of existing users the commandsphp artisan sync-achievements comment_written
andphp artisan sync-achievements lesson_watched
help them sync their achievements.
- The achievements have a type of
- No any third party library other than the framework itself provides.
- Uses php 8.0, but php >= 7.4 can also be used.
- Uses phpunit for testing and xDebug 3.0 for code coverage analysis.
Run the following commands to set up the application, given that php
and composer
are available:
git clone
https://github.com/ravisharmaa/badges-and-achievements.gitcd badges-and-achievements
composer install
cp .env.example .env
vendor/bin/phpunit
- For code coverage:
vendor/bin/phpunit --coverage-text
- Additionally a report can also be generated using the command
vendor/bin/phpunit --coverage-html=reports
The current code coverage is 91%. Given the laravel frameworks code which show uncovered while using xDebug.
The tests provide a basic overview of the application. Some steps can be done to see the application in action, which are.
- The data for comments and lessons can be seeded via Tinker and achievements and badges can be seen.
php artisan tinker
-
$user = User::factory()->create(); Comment::factory()->count(10)->create([ 'user_id' => $user->id, ]); $user->lessons()->attach(Lesson::factory()->count(10)->create(),['watched' => true]); php artisan sync-achievements comment_written php artisan sync-achievements lesson_watched
- Which results in the output for url
/users/1/achievements
{ "unlocked_achievements": [ "First Comment Written", "3 Comments Written", "5 Comments Written", "10 Comments Written", "First Lesson Watched", "5 Lessons Watched", "10 Lessons Watched" ], "next_available_achievements": [ "20 Comments Written", "20 Lessons Watched", "25 Lessons Watched", "50 Lessons Watched" ], "current_badge": "Intermediate", "next_badge": "Advanced", "remaining_to_unlock_next_badge": 4 }
- I have used an implicit way of storing achievement, and badge names into the database while a user's achievements are calculated via the event. The constructor for each achievement add the necessary name for the achievement. An admin might want to edit their properties which currently, is a bit difficult. If there were a proper backend application which could help manage updates and addition of badges and achievements but it was out of the scope regarding the task and given time frame.
- I have also added a property
achievement_type
in the database which seems redundant as it is used in the achievement class and in the table. Nevertheless, it might help in grouping the users via their achievement types. - I have also assumed that the next badge is in an incrementing order, which might not be the case always. I thought of adding a
next_badge
in the badges table but somehow opted from that. - There are places to improve. Still, I would not opt to do them all for a small problem domain like this one. As software engineers we need to find a balance.
It was a challenge. However, if I had to improve upon this:
- I would work out to find out a proper way to store next badges.
- Achievements could be editable, or a command to create the achievements and the related class would be great! The developer then only had to add the related class and qualifier logic and be done.
- For a team project, it will be good to have the project dockerized.