Dev Journal
Table of contents
Loading...Introduction
This dev journal is intended for documenting some changes made and the corresponding code modifications.
It may serve as an inspiration for future feature implementation and bug fixes.
Splitting ClientResult into use case specific DTOs
The ClientResultData object was being used for the client read and the client list request.
This was done for convenience because the result for client list has the same attributes as for
client read it just doesn't need all of them. I wanted to save
time and not create an extra object with its own specificities.
The issue
The problem is that it goes against the Single Responsibility Principle. For every little change, all the use cases where the result DTO is being used are affected, which makes it more prone to lead to unexpected side effects.
Probably the risk isn't huge yet with two use-cases and few attributes, but in the future
each use-case will probably have more specific attributes.
This template application is intended
to be as "clean" as possible for it to be easily scalable. Better do it right from the start instead of
having more work refactoring later when it's getting bigger.
What does have to be changed
The ClientResultData should be split into two separate DTOs, one for each use-case.
Change steps
Commit: d4a7ac2f484839a8f2b7dd5159c59fafca6d2371
- Rename
ClientResultDataintoClientReadResultmake a copyClientListResult. - Check where the instance
ClientReadResultis used and if it is for the client list use-case, replace with the newClientListResult. - The next step is to find out which attributes are needed for both use cases
- Check where
ClientReadResultis used and what attributes are needed- For this, ideally we can click on the attributes with the mouse middle wheel or
Ctrl + clickbut this only works if the DTO is being used by a PHP template and not if it's requested via an AJAX request. - To reliably find out where the attributes are being used, we have to check where the
ClientResultDatainstance is being used. - In the end, the values are used by
the frontend so the Action that returns the read result data is most relevant. Actions get their data
from a domain service, and there is only one service function that returns the
ClientReadResult:ClientFinder::findClientReadAggregate(). This function is used by theClientReadPageActionwhich renders theclient-read.html.phptemplate. - There we can look for the attributes that are being used. It's a rendered template, and we now
know that it's the only place where
ClientReadResultis being used, so we canCtrl + clickon the attributes in the DTO directly to check if they're needed in the template or not.
- For this, ideally we can click on the attributes with the mouse middle wheel or
- All are needed except the array
$noteswhich is removed. - Check where
ClientListResultis used- Steps are similar, but this time the DTO is serialized into JSON and returned via an AJAX request.
- The
ClientListResultis used by theClientFinderRepository::findClientsWithResultAggregate()and following the path backwards to the Action we findClientFetchListActionwhich returns theClientListResultas JSON. This action is invoked by the route name GET/clients. - Searching for 'clients' in the JS-files associated with a GET request, we find the
client-list-loading.jsfetchClients()function and the JSON response is used infetchAndLoadClients(). - The client collection of this JSON response is used in
getClientProfileCardHtml()of theclient-list-profile-card.html.jsfile.
- We can see that appart from the client DTO item values (i.e.
first_name,last_name, etc.), only theassignedUserPrivilegeandclientStatusPrivilegeare used. - So every attribute from
ClientListResultcan be removed except these two. - Now the place where the
ClientListResultis populated (ClientFinderRepository::findClientsWithResultAggregate()) can be adapted to only populate it with the needed attributes. - In the
ClientFinderRepositoryI tried to avoid having to use the same field strings twice in each select query for both the client read and client list find function. I made them to be attributes of the class so I could re-used them, but it made everything so much more complex and rigid. Now both functions select exactly their needed fields and the fact that thefindClientsWithResultAggregatefor client list contains some of the exact same names than for client read does not bother me any more. It's the opposite, I find it so much better because each one can be updated without having to worry about the other use case at all. And if there is a change in field name that affects both they can easily be changed withCtrl + Shift + R(search and replace) in PHPStorm. - The
ClientListActionTesthas to be modified to assert only the needed attributes.
Adding a simple new field theme
The user can choose a theme that should be saved in the database.
There are a few places where the code has to be modified, and the field added
so that it works as expected.
The value can only be added by update, so we only need to
change one action.
Recap
UserTheme.phpcreate theme enum.UserValidator.phpadd validation line invalidateUserValues().UserAuthorizationChecker.phpadd to the$grantedUpdateKeysinisGrantedToUpdate().UserUpdater.phpaddthemeto the authorized update keys inupdateUser().UserData.phpadd instance variable and populate in constructor.UserFinderRepository.phpadd to the class attribute$fields.UserUpdateProvider.phpadd theme change to$basicDataChangesvar inuserUpdateAuthorizationCases().UserUpdateProvider.phpadd theme change to request body and error message to response ininvalidUserUpdateCases().
Implementation
-
The field is stored as enum in the database, and it is possible that one day there will be other themes than just light and dark, so it makes sense to create a php enum:
// Domain/User/Enum/UserTheme.php enum UserTheme: string { use EnumToArray; case light = 'light'; case dark = 'dark'; }- The service function
updateUser()is called which first validates the valuesvalidateUserUpdate(). As it's a backed enum the functionvalidateBackedEnum()can be used:// UserValidator.php $validator // ... ->requirePresence('theme', false, __('Key is required')) ->add('theme', 'themeIsAvailable', [ 'rule' => function ($value, $context) { // Check if given user status is one of the enum cases values return in_array($value, UserTheme::values(), true); }, 'message' => __('Invalid option'), ]);
- The service function
- When value validation is done, authorization is tested with
isGrantedToUpdate(). It checks if authenticated user is allowed to change given field. The way it works is by adding the field name to a$grantedUpdateKeysarray if the permissions match.
themehas the same authorization rules as the other "general user data" so it's added right below the list:if (array_key_exists('theme', $userDataToUpdate)) { $grantedUpdateKeys[] = 'theme'; } - The service function
updateUser()creates an array of the fields that may be updated to be certain that only the fields that are designed to be updated actually get changed:if (in_array($column, ['first_name', '...', 'theme'], true)) { $validUpdateData[$column] = $value; } -
Now to be able to read this value and so the app knows that
themeis now a user value, add it toUserData.phpinstance variables and also the constructor.class UserData implements \JsonSerializable { /* ... */ public ?UserTheme $theme = null; public function __construct(array $userData = []) { $this->theme = $userData['theme'] ?? null ? UserTheme::tryFrom($userData['theme']) : null; } } - The file that retrieves the user values from the database is the repository
UserFinderRepository.phpand it retrieves only the requested values, not automatically all fields. The list of values that should be selected are in the class attribute$fields:class UserFinderRepository { private array $fields = ['id', '...', 'theme',]; // ... }Testing
- User theme value is an extremely simple, value therefore not much has to be done especially when
there are good existing tests.
The test itself doesn't even has to be changed, only the data provider that feeds it valuesUserUpdateProvider.php:public static function userUpdateAuthorizationCases(): array { // ... $basicDataChanges = ['first_name' => 'NewFirstName', '...', 'theme' => 'dark']; // ... } - There is a validation being made so an invalid value should be tested as well. For this the function
invalidUserUpdateCases()can be extended to also include'theme' => 'invalid',in the request body and['field' => 'theme', 'message' => 'theme not existing'],in the response validation errors.