SDSM:Historical Changes: Difference between revisions
(Add sub-sections on dependency changes; prior edit added section on consolidate model classes) |
(Add entry on severing the design flaw of Eloquent coupling between PHP class names and the database) |
||
Line 84: | Line 84: | ||
referred to in other code, such that any which weren't referred to by their | referred to in other code, such that any which weren't referred to by their | ||
fully-qualified names before were referred to in that way after. | fully-qualified names before were referred to in that way after. | ||
[[#top|RETURN]] | |||
=== 2024 May 8: Sever Coupling of PHP Class Names to Database Data === | |||
Laravel Eloquent has a particular more-advanced feature for defining | |||
relationships between model classes that it refers to as a | |||
''polymorphic relationship'', such that a relationship is not just between | |||
2 specific model classes, but rather between 1 model class and 1 set of all | |||
model classes that fulfill a particular role. | |||
https://laravel.com/docs/8.x/eloquent-relationships#polymorphic-relationships | |||
Prior to the performance of this task, ''SDS Laravel'' utilized such an | |||
Eloquent relationship, between <code>Group</code> on one side, and the set | |||
comprising <code>Page</code> and <code>SubPage</code> on the other side. | |||
And it did so in a design-flawed way that led to tight coupling between the | |||
exact fully-qualified PHP class names of the latter 2 model classes, and | |||
the database data, such that those fully-qualified class names | |||
(<code>App\SubModel\Page</code> and <code>App\SubModel\SubPage</code>) were | |||
stored in the database (specifically in the <code>group_pages</code> | |||
table's <code>class_type</code> column) and the fact that these matched was | |||
relied on by the application for correct behavior. | |||
This tight coupling manifested as broken app behavior when attempting to | |||
move/rename the <code>Page</code> and <code>SubPage</code> classes (to | |||
<code>App\Models\Sub\Page</code> and <code>App\Models\Sub\SubPage</code>), | |||
because then their names no longer matched what was in the database. | |||
The most common kind of breakage was that, for almost every app screen that | |||
was gated with a user group privilege check for the screen, the app thought | |||
no user had any privilege to use it; for example, this error message | |||
displayed when trying to app general search screen: | |||
No access to Search.index | |||
You do not appear to have permission to access https://api-latest.smus.ca/Search as [User's Name]. If you believe you should have access | |||
to open a help ticket request permission, or you can go back / home and try again. | |||
And had users been able to get past that, certain other screens or | |||
functionality that rely on displaying or editing user group privileges for | |||
pages and subpages would have had problems, such as failing to see existing | |||
privileges made before the rename (they might have worked to re-add the | |||
privileges, then using the new hard-coded class names in the database). | |||
Without correcting the design flaw, renaming these model classes would have | |||
required a corresponding database migration to replace the old | |||
hard-coded-in-data PHP class names with their new ones, which carried more | |||
complexity and risk. | |||
The more specific design flaw was the use of Eloquent's | |||
<code>morphToMany()</code> relationship builder without specifying an | |||
explicit constant string to represent each morphed class in the database, | |||
resulting in Eloquent defaulting to using its fully-qualified PHP class | |||
name instead. | |||
Both <code>Page</code> and <code>SubPage</code> had a copy of this method | |||
(this version is post-rename) which was affected: | |||
public function groups(){ | |||
return $this->morphToMany(\App\Models\Sub\Group::class, 'class', 'group_pages'); | |||
} | |||
This task made the simplest possible fix to this design flaw by explicitly | |||
configuring Eloquent to use an explicit constant string per each morphed | |||
class, by adding the following in the file | |||
<code>app/Providers/AppServiceProvider.php</code>: | |||
Relation::enforceMorphMap([ | |||
'App\SubModel\Page' => 'App\Models\Sub\Page', | |||
'App\SubModel\SubPage' => 'App\Models\Sub\SubPage', | |||
// TODO: Migrate database to use non-PHP-class-looking slugs | |||
// like following instead so they don't look tightly coupled. | |||
// 'page' => 'App\Models\Sub\Page', | |||
// 'sub_page' => 'App\Models\Sub\SubPage', | |||
]); | |||
This was the simplest possible fix, and ostensibly the most elegant, by | |||
working with the system, using an Eloquent feature expressly designed for | |||
this very scenario, rather than say trying to avoid using the | |||
<code>morph</code> features (although doing so may be better longer term), | |||
and also by not requiring any database changes, so it works with the | |||
database as-is. | |||
https://laravel.com/docs/8.x/eloquent-relationships#custom-polymorphic-types | |||
Following the changes of this task, there was still room for an even more | |||
elegant solution to follow that alters the database to use a | |||
non-php-class-looking constant in the database data to represent each | |||
class, so the app isn't forever using the old class name there, but that | |||
was considered higher complexity and risk and suited to be done as a | |||
separate task another day. | |||
Note that a consequence of using <code>Relation::enforceMorphMap()</code> | |||
is that it makes Eloquent treat the entire app more strictly, such that if | |||
we try to use <code>morphToMany()</code> or similar Eloquent methods with | |||
any other classes besides <code>Page</code> and <code>SubPage</code>, it | |||
will require us to add an entry to the above list for it, rather than | |||
silently defaulting to the old behavior for other unspecified classes. I | |||
consider this strictness to be a good thing. | |||
[[#top|RETURN]] | [[#top|RETURN]] |
Revision as of 19:26, 9 May 2024
This document consists of multiple parts; for a directory to all of the
parts, see SDSM:Index.
Description
This part of the SDS Modernization (SDSM) document enumerates a not necessarily exhaustive list of historical changes or improvements that were made to SDS, made by Darren Duncan if by whom is not otherwise specified.
It exists to provide visibility into historical progress of the SDS Modernization (SDSM) project about which this document otherwise mainly just describes the current state of SDS.
This list often but not always corresponds to the list of Git pull requests at https://git.smus.ca which most readers of this document are not privileged to see directly.
SDS Laravel: Fixes to Broken Behavior
2024 Apr 18: User Preferences Screen Fails to Display
Context: User darren.duncan
attempted to visit their
User Preferences screen, example urls
https://api.smus.ca/User/Preferences
(production) and
https://api-latest.smus.ca/User/Preferences
(preview).
Observed Behavior: In production, a non-graceful generic 500 internal
server error message was displayed rather than the expected User
Preferences screen. In preview, the revealed underlying symptom was that
the code line in PreferencesController
of
$addresses = $user->guardian->addresses;
was dying with
Attempt to read property addresses on null
. The problem
occurred for user darren.duncan
but not for other users.
Expected Behavior: The User Preferences screen should always display, regardless of whether the user in question is a guardian or has guardians. If some functionality of the screen is not applicable for users that aren't or don't have guardians, the screen should either hide that functionality or explain why it doesn't apply or both.
Applied Solution: The dying code line was replaced with code that only tried to fetch and return an address list when it was valid to do so, and it returned an empty list otherwise. As a result, the User Preferences screen never failed to display under this scenario.
Caveat: While the User Preferences screen no longer fails to display,
as of the completion of this fix, it still remained true that the
functionality to actually display the fetched address list in question was
never implemented, and so for every user, the screen carried the
placeholder message Shows address info
instead of each
individual address.
SDS Laravel: Internal Design Changes
2024 Apr 19: Consolidate Eloquent model classes to /app/Models
An idiomatic PHP Laravel project has its main files grouped into the specific hierarchy of folders documented here:
https://laravel.com/docs/11.x/structure
Of particular relevance, the folder /app/Models
is where all
of the Laravel Eloquent model classes go.
Prior to the performance of this task, SDS Laravel conformed to the
idiomatic folder layout in all ways except with respect to the Eloquent
model classes, which it had instead spread across about a dozen
/app/FooModel
folders.
This task moved/renamed all of the Eloquent model classes, such that for
each class /app/FooModel/Bar.php
, it was moved/renamed to
/app/Models/Foo/Bar.php
. While that described nearly all of
those classes, a handful started with other names, but they were
appropriately gathered under /app/Models
too.
This task also included a normalization of how first-party classes were referred to in other code, such that any which weren't referred to by their fully-qualified names before were referred to in that way after.
2024 May 8: Sever Coupling of PHP Class Names to Database Data
Laravel Eloquent has a particular more-advanced feature for defining relationships between model classes that it refers to as a polymorphic relationship, such that a relationship is not just between 2 specific model classes, but rather between 1 model class and 1 set of all model classes that fulfill a particular role.
https://laravel.com/docs/8.x/eloquent-relationships#polymorphic-relationships
Prior to the performance of this task, SDS Laravel utilized such an
Eloquent relationship, between Group
on one side, and the set
comprising Page
and SubPage
on the other side.
And it did so in a design-flawed way that led to tight coupling between the
exact fully-qualified PHP class names of the latter 2 model classes, and
the database data, such that those fully-qualified class names
(App\SubModel\Page
and App\SubModel\SubPage
) were
stored in the database (specifically in the group_pages
table's class_type
column) and the fact that these matched was
relied on by the application for correct behavior.
This tight coupling manifested as broken app behavior when attempting to
move/rename the Page
and SubPage
classes (to
App\Models\Sub\Page
and App\Models\Sub\SubPage
),
because then their names no longer matched what was in the database.
The most common kind of breakage was that, for almost every app screen that was gated with a user group privilege check for the screen, the app thought no user had any privilege to use it; for example, this error message displayed when trying to app general search screen:
No access to Search.index You do not appear to have permission to access https://api-latest.smus.ca/Search as [User's Name]. If you believe you should have access to open a help ticket request permission, or you can go back / home and try again.
And had users been able to get past that, certain other screens or functionality that rely on displaying or editing user group privileges for pages and subpages would have had problems, such as failing to see existing privileges made before the rename (they might have worked to re-add the privileges, then using the new hard-coded class names in the database).
Without correcting the design flaw, renaming these model classes would have required a corresponding database migration to replace the old hard-coded-in-data PHP class names with their new ones, which carried more complexity and risk.
The more specific design flaw was the use of Eloquent's
morphToMany()
relationship builder without specifying an
explicit constant string to represent each morphed class in the database,
resulting in Eloquent defaulting to using its fully-qualified PHP class
name instead.
Both Page
and SubPage
had a copy of this method
(this version is post-rename) which was affected:
public function groups(){ return $this->morphToMany(\App\Models\Sub\Group::class, 'class', 'group_pages'); }
This task made the simplest possible fix to this design flaw by explicitly
configuring Eloquent to use an explicit constant string per each morphed
class, by adding the following in the file
app/Providers/AppServiceProvider.php
:
Relation::enforceMorphMap([ 'App\SubModel\Page' => 'App\Models\Sub\Page', 'App\SubModel\SubPage' => 'App\Models\Sub\SubPage', // TODO: Migrate database to use non-PHP-class-looking slugs // like following instead so they don't look tightly coupled. // 'page' => 'App\Models\Sub\Page', // 'sub_page' => 'App\Models\Sub\SubPage', ]);
This was the simplest possible fix, and ostensibly the most elegant, by
working with the system, using an Eloquent feature expressly designed for
this very scenario, rather than say trying to avoid using the
morph
features (although doing so may be better longer term),
and also by not requiring any database changes, so it works with the
database as-is.
https://laravel.com/docs/8.x/eloquent-relationships#custom-polymorphic-types
Following the changes of this task, there was still room for an even more elegant solution to follow that alters the database to use a non-php-class-looking constant in the database data to represent each class, so the app isn't forever using the old class name there, but that was considered higher complexity and risk and suited to be done as a separate task another day.
Note that a consequence of using Relation::enforceMorphMap()
is that it makes Eloquent treat the entire app more strictly, such that if
we try to use morphToMany()
or similar Eloquent methods with
any other classes besides Page
and SubPage
, it
will require us to add an entry to the above list for it, rather than
silently defaulting to the old behavior for other unspecified classes. I
consider this strictness to be a good thing.
SDS Laravel: Changes to Third-Party Dependencies
2024 May 3: Stop requiring dbal, revisionable-upgrade, laravel-cors
Prior to the performance of this task, the SDS Laravel Composer config
file composer.json
explicitly required these 3 third-party
PHP library dependencies, which were not actually used by the app:
- doctrine/dbal (^3.5)
- fico7489/laravel-revisionable-upgrade (*)
- fruitcake/laravel-cors (^2.0)
This task updated composer.json
to remove those unused
explicit dependencies, which also had the effect of removing 6 other
Composer-resolved dependencies.
2024 May 6: Reflect rename of fzaninotto to fakerphp
This task updated composer.json
to reflect that the
third-party PHP library dependency fzaninotto/faker
was
renamed to fakerphp/faker
(in fact, that rename occurred prior
to the release of Laravel 8), and also require its latest version.
2024 May 6: Upgrade PHPUnit from 9.x to 10.x
This task updated composer.json
to require the next/newest
major version of the PHP dev dependency PHPUnit from 9.6.19
to
10.5.20
.