Problem/Motivation
core/modules/update/update.compare.inc contains 1 method, update_process_project_info() that is called from outside of the file.
update_calculate_project_update_status() is almost 300 lines long and its docblock is over 60 lines. It very hard to follow what this function is doing.
Now that we did #3094304: Create tests that cover contrib non-full releases and contrib patch versions we have much better test coverage for this functionality
In #3100115: The update module should recommend updates based on supported_branches rather than major versions majors we will be changing much of the logic of update_calculate_project_update_status(). If this isn't in a class then we either have to add to the existing method or add more global functions. If we move to the class within #3100115 then reviewing the changes becomes much hard because the diff of the refactor for the new logic around support branches would not be clear.
Automatic Updates will also additional information from the Update XML that would easier to get if this function was already refactored. See #3252187: [Meta] Deal with core Update module recommending the latest release in the major version not the latest in the Minor version and #3254207: Only allow 1 patch release update increment in Cron
Proposed resolution
Instead of just creating 1 new class that encapsulates all update_calculate_project_update_status() we should take all the logic possible out of this method that doesn't deal directly with assigning values to the specific elements of the $project_data array and move this new ProjectStatusCalculator class.
ProjectStatusCalculator would have these public methods
getLatestDev() currently set as <code>$project_data['latest_dev']if this is either the latest development release of the target major or the latest release, which ever is more recentgetRecommendReleaseForTargetMajorcurrently set as$project_data['recommended']the recommended version for the target major. This may be the current version is there is not recommended updategetLatestReleaseForTargetMajor()currently set as$project_data['latest_version']getReleasesInMajorsGreaterThanTarget(better name?) currently set as$project_data['also']now. This is the latest version for each target major greater than the current target majorgetSecurityReleases()set as currently set as$project_data['security updates']getStatus(). This does not directly correlate to what is stored in
$project_data<code>.... Because for instance <code>$project_data['status'] = UpdateManagerInterface::NOT_SUPPORTEDappears multiple times. Once it is stores
project_statusfrom the XML, another time it is because the installed version is not in a supported branch and another time it is because installed version is marked unsupported.for more details see #33
So this will return the status based on what the currently has installed. Not on the status of the Update XML.
getInstallType()a wrapper around$project_data['install_type']
All of these public methods will not return any versions that should never be installed as defined by being, insecure, unpublished, revoked, unsupported or or an unsupported branch. They all rely on private method getInstallableReleases() to ensure that they don't consider uninstallable versions. In the future we could make this public so that we could #3252190: Store all installable releases in update_calculate_project_update_status() which allow Automatic Updates to select recommendations use different criteria while not having to worry figuring out which versions should never be recommended.
Nothing in ProjectStatusCalculator should be considered an API except the public methods therefore it should be final and all non-pubic internals should be private.
Remaining tasks
Review
User interface changes
None
API changes
Introduces ProjectStatusCalculator class we should consider if this should be @internal or now.
Data model changes
None
Release notes snippet
Note needed
| Comment | File | Size | Author |
|---|
Issue fork drupal-3100110
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
tedbowWe should get #3074993: Use "current" in update URLs instead of CORE_COMPATIBILITY to retrieve all future updates in first because it is critical
We should also get #3099825: Test available updates when the current major and next major of core are supported in first because it adds more test coverage for what we would be refactoring here
Comment #3
tedbow#3074993: Use "current" in update URLs instead of CORE_COMPATIBILITY to retrieve all future updates is RTBC. I would really love to do this issue next before #3100115: The update module should recommend updates based on supported_branches rather than major versions majors which will be a blocker for Drupal.org supporting semantic contrib releases but I don't also want to waste alot time refactoring
update_calculate_project_update_status()because #3100115 will again change it.If we do #3100115 first I fear we are just going to make
update_calculate_project_update_status()more of jumbled mess.So this patch on top of #307499 simply moves
update_calculate_project_update_status()into a new class as\Drupal\update\UpdateProjectStatus::getUpdateProjectData().The logic has not changed at all. My hope would be that we can get this in and then in #3100115 we are free to move our new logic into private functions to make it more understandable.
Comment #4
tedbowupdating title.
update_calculate_project_update_status()is the only function that deals with 1 project at a timeComment #5
tedbow#3074993: Use "current" in update URLs instead of CORE_COMPATIBILITY to retrieve all future updates was committed 🎉
Here is with a couple doc updates.
Notice I didn't update the doc for
\Drupal\update\UpdateProjectStatus::getUpdateProjectData()I copied this directly from the current function. There are probably problem with it but the logic of the function didn't change at all. So I don't think we should go down that rabbit hole.Comment #6
dwwFirst of all, thanks for trying to make sense of my 10+ year old giant function, and trying to reorganize it to be more maintainable. If only Drupal was OO friendly and API-by-array wasn't the Drupal Way(tm) to do everything when I wrote all this in the first place, perhaps you wouldn't be faced with this annoying task now. ;)
The comment implies this function is still "the heart of the update status feature", yet it's a 2-liner that calls a private method and returns the results.
a) Seems like a weird code organization. Why bother with the private method at all? It's only called exactly once. The whole class is "final" and "@internal", and the API of the public function (@return array) can remain the same even if we further refactor the method or change implementation details.
b) If we're set on having a private method that does the heavy lifting, seems we should move the detailed docblock to document the function that actually does what the docs explain. The documentation on the public method could be simplified to explaining the structure of the returned array in detail, which is mostly what callers care about.
Since part of the goal is prepping for semver, we should adopt the @mixologic et al idea that previous/current Drupal contrib versions are API-MAJOR.MINOR, not API-MAJOR.PATCH.
We should probably update these from 5.x examples, too. ;)
"updated" how? If we're going through the trouble to make this a class, let's document what changes to projectData we're actually making.
a) This is still a huge function. Is the plan to have a follow-up issue to actually do what the summary says: "The class should refactor
update_calculate_project_update_status() into smaller methods that can be more easily understood."?
Comment #3 says: "The logic has not changed at all. My hope would be that we can get this in and then in #3100115 we are free to move our new logic into private functions to make it more understandable."
So either the summary needs an update or the patch needs more refactoring. I'd personally prefer the later, since the premise of this issue is that this logic could be split up into smaller pieces to be more easily understandable by someone who didn't write it. ;) If that's true, let's see a proposed reorganization that actually works and results in something with smaller methods. If we're making this a class just because we like classes, but it turns out that you still need a giant function to do everything we need, I'm less inclined to try to get this into core at all.
Since this is now an object, do we care about t() or should we inject the right t() method to use?
Nits: extra space before $this and longer than 80 chars.
Not the fault of this patch, but "createFromSupportBranch()" is a weird name for this method. Should have been "createFromSupportedBranch()" -- the branch is either supported (by the maintainer) or not. It's not a "support branch"... *shrug*
Extra newline.
s/support/supported/
This return is unneeded.
This is the central API change of this patch. We used to have a method that took the project_data array by reference, did a bunch of computations and comparisons, and updated the array in place with the new information.
Now, we instantiate a new object with both the project data and the available releases data, call an (oddly named) method to process the project array, and overwrite the array we started with. I'm not totally convinced this is a win for code readability or maintenance. I'm sure it's a small (but perhaps measurable) step backwards in the required memory footprint of this module. I sure hope PHP's garbage collection is working properly and it cleans up all the memory used by the protected arrays in each of these UpdateProjectStatus objects.
Regardless, seems like a lot of churn and copying to still have a 300+ line function. ;)
Thanks/sorry,
-Derek
Comment #7
dwwp.s. re: "There are probably problem with it but the logic of the function didn't change at all. So I don't think we should go down that rabbit hole."
If we're reorganizing all this code, I think we have to update the docs to match. When will we "go down that rabbit hole" if not now? IMHO, this patch is of almost no use if it just wraps the huge function (and comment) in class syntax but otherwise leaves it as-is. It has the potential to really help mere mortals contribute to the update module. But in its current state, it doesn't help that goal at all, and perhaps is even a step backwards...
Comment #8
tedbow@dww thanks the review. It is amazing that this 10+ year old code has served us well for so long!
re #6
But I agree the private method was weird and just changing the return statements is better.
Since the logic is not changing at all and the only code that is changing is the return statement rather relying on the $project_data reference I think fixing the comment is not necessary. Also there are probably a bunch of stuff we could fix about the comment and I would rather not get into that I feel like they should have been handled in #3074993: Use "current" in update URLs instead of CORE_COMPATIBILITY to retrieve all future updates if we missed something we should add follow-up to that. But hopefully I think follow-ups are necessary since this will change anyways in 3100115
Putting this into a class so we don't have to do that and the update logic in #3100115: The update module should recommend updates based on supported_branches rather than major versions majors. I would like keep 3100115 to only the logic changes needed and not moving it into a class. It is going a lot of changes in that issue and would like to make it as small as we can so we get it right.
I have started to look at #3100115 and was working on patch on top of this. Here is rough non-work version of this class I was working on https://github.com/tedbow/drupal/blob/3100115-from-3100110-5/core/module...
note this doesn't have fixes above and probably others but it might give some idea of the new logic that could be split out into new methods
You could have 2.1. or 2. branch this actually after 8.x-1. so the comparison is tricky and benefits from a dedicated docblock
again with branches and version in different formats it is tricky
This is all could maybe wrapped in a new method
getTargetSupportBranch()because it is all more complicated then the existing logic for getting the target major. I think this demonstrates why I want have this all i class before #3100115. If the logic was moving files and being split out into these new functions the patch would be much harder to review.This is all I have so far and I didn't look into refactoring any of the existing logic that really isn't going to change. That could be non-urgent follow.There is probably the needed for other new methods that I have encountered yet. But I think not adding the new logic directly in the main function is already a clear enough benefit.,
::$projectDataso now it is below 80 charsre #7 I think we will go down the rabbit in #3100115 and that fact that all the logic change will be in 1 file will just make it easier to review.
Comment #9
tedbowupdated the summary.
the only other thing I wondering is if we should actually more all the methods in
update.compare.incinto the class. The new version of\update_calculate_project_data()would be the only method that would need to made public. Nobody should be calling\update_calculate_project_update_status()directly But that would mean\update_calculate_project_update_status()could not call the new version in the and be deprecated. It would have to removed. Maybe we can't do thatComment #11
dwwRe: #8: Okay, I guess I need to look more closely at #3100115: The update module should recommend updates based on supported_branches rather than major versions majors ;)
Meanwhile:
Nit: Aren't we supposed to have a newline between the 1-liner and the @param statements?
Thanks for adding these docs!
You "typehint" most of the keys, but not these. Can we be consistent in the format / content of this info?
None of the other keys are 'quoted' like this. Any reason why you did that here?
Need an extra space before "will" on the 2nd line for it to line up...
Dumb question: are these actually keyed by "major" version number now? ;) I've lost track...
WTF? ;) Search/replace gone wrong? I think all of these are missing a
returnin front of them? Sure hope the bot fails #8. ;)I don't think this can be true. We're not adding new deprecations in 8.9.x that are immediately removed in 9.0.0.
Comment #12
dwwHah, x-post with the bot. Phew! Glad that failed. The fails even look reasonable on a quick skim given the bugs in #8 with the missing returns.
Comment #13
tedbowI had a bunch of these where I meant search/replace
return;forreturn $this->projectDatabut somehow messed it up.Comment #14
dwwInterdiff in #13 looks like it fixes #11.7. Everything else in #11 is still to-be-addressed.
Thanks,
-Derek
Comment #15
tedbowre #14 yeah I was just addressing test failure didn't notice you had already posted another review
re #11
@dww thanks for the review
but now see the ultimately come from the xml. Adding type hints and removing the comment about where they are copied from.
also update
fetch_status(string)figured out this comes from the xml directly$this->projectData['also'][$release_major_version] = $version;I think the idea here is you maybe in support major like 8.x-1.x but there multiple new majors like
8.x-2.xand8.x-3.xI guess we need to make change record unless update_calculate_project_update_status() was never to be called directly.(also see #9)
Comment #16
tedbowI talked with @tim.plunkett about this issue too. He had some the concerns that @dww had about just making it a class without actually refactoring but also saw my point about #3100115: The update module should recommend updates based on supported_branches rather than major versions majors being very hard to review if we don't make the new class first.
Really refactoring this into a value object would be bigger task than we probably have time for now. Because then we would need to update a lot more places in the update module that use this project info.
So as an alternative is we could really just make a new utility class that has a public static method that keeps a similar signature as
update_calculate_project_update_status()in that takes the project data by reference. So instead of doing$projects[$project] =(new UpdateProjectStatus($projects['drupal'], $available['drupal']))->getUpdateProjectData()We would do something like
UpdateProjectCalculator::calculateStatus($projects['drupal'], $available['drupal']);This would still make it possible change the logic in #3100115: The update module should recommend updates based on supported_branches rather than major versions majors and split the new logic into methods without introducing new global functions(which then become defacto apis) and not adding a bunch of new anonymous functions like we added
$is_in_supported_branchthat are more difficult to document.I will work on this.
Comment #17
tedbowhere is the idea in #16 basically changing a value object to a static helper function.
I haven't had a chance to fix coding standard issues but this should at least show the idea
Comment #18
tedbowFixing coding standards for comments. I didn't fix everything in the original function but since it is indented more I fixed that.
Comment #19
tedbow🤷♂️👽
Comment #20
tedbowI suggested
update_calculate_project_data()as an alternative because this is the only place the originalupdate_calculate_project_update_status()was called. Before it was called we haveAnd the logic expects this. So it seems like
update_calculate_project_data()should be the entry point for the API.if a module needed to call it just for 1 module they could do
update_calculate_project_data([$available['my_module']])They would have until Drupal 10 to fix this.
Comment #21
dwwA) Some other patches that touch this function have already landed, and hopefully #2992631: Update report incorrectly recommends security releases for old minors when a security update is needed and a secure version of the old minor is also available will also land soon. So this will need a re-roll at least. Should probably wait for #2992631 to land first.
B) After working on a bunch of recent Update manager issues, some of which deal with obscure bugs in this function, I'm getting a sense of ways we could split this up to be more understandable and less buggy/fragile. This function does 4 main things:
It currently does all 4 jobs in one convoluted pass through the release history XML for a given project. Originally, this was an attempt to be more efficient, use less RAM, spend less time computing all this stuff, etc. But weird interactions between the 4 leads to bugs like #2992631. In exchange for spending more cycles (and potentially eating more RAM), we could have something easier to make sense of and more resilient. We could iterate over the release history 4 times, in 4 separate/targeted protected helper functions, that each handle one of these jobs. Then we're not mixing up responsibilities, there will be no weird side effects, it'll be easier to document (and test!) each method, and each one will be a lot shorter and less magical than what we have now.
If we do *that* as part of this issue, I think we'll really be gaining something valuable. Short of that, I think we're just playing with OO syntax for little or no benefit.
Cheers,
-Derek
p.s. Edit: And we wouldn't need to iterate the 4th time for -dev unless you're on a -dev release, so for most projects on most sites, we'd only iterate thrice.
Comment #22
dwwAdding #3111767: API to provide a recommended next version for project as also related. If we do it right, that could be solved by #21B.2
Comment #25
tedbowCreated a related issue #3206293: Create ProjectRelease class to represent Update XML releases
I am starting to work on the Automatic Updates functionality and found myself accessing the release data via array keys. So I think that would something nice to do before we have the new module. Also I think it makes the current function
update_calculate_project_datamore readable.Comment #29
tedbowPushing up my work in progress on another idea.
My previous idea was just swap this function directly for a class but now I think it is not a great to just pass in
$projects[$project]by reference and have it add values directly to this array.So I started a Merge Request from scratch that has a new class called
ProjectStatusCalculatorthat will have well defined methods likegetRecommendReleaseForTargetMajor()So the new merge request will leave
update_calculate_project_update_status()in place for now but replace a lot of its logic with calls to the methods on ProjectStatusCalculatorComment #30
dwwThanks for working on this again!
Before you get too far on a whole new approach, what do you think of my proposal in #21 on how to reorganize the logic?
Thanks again,
-Derek
Comment #31
tedbowI think the new MR kind of handles points 2) and 3) from #21
I am not opposed to the other points but I am not sure we have to do them all in 1 issue. If we only handled part of it I think we would want to rescope this issue.
from #21
That is very similar to what I am doing in this MR.
the new
allows 4 separate new public functions on the class to not have handle the actually looping and filtering of releases that should not be consider in any of the cases, and aren't now.
Also because it keeps
static $releaseseven though each of the helper functions can all it but iterating through all releases will still only happen once.I have to take off now but will look back more at #21 to see how it compares to this and leave a more detailed comment
Comment #32
tedbowNote more tests are passing with the last few commits
PHP 8.0 & MySQL 5.7 6 pass, 8 fail
Only show if the test class all pass not individual test methods.
Comment #33
tedbowI thinking has evolved since I first made this issue 😜
re @dww in #7
I mostly agree with this now. It would be of some use but I won't leave us in a much better state. My first plan was we would then iterate over the class in follow-up issue but.....
re @dww in #6
I think another problem with my patches in #18 and before I was still making a class that was just passing the
$project_dataarray by reference and altering it so continuing at our API-by-array problemIn the current merge request(1537) I have changed this to follow the pattern we used in #3206293: Create ProjectRelease class to represent Update XML releases. We created the public factory method
\Drupal\update\ProjectRelease::createFromArray()and kept the constructorprivate. This way it allows in the future, if it makes sense, when we are able to not depend on the array as API to deprecatecreateFromArray()and either create another factory or make the constructor public. Then everywhere we need information about the release we no longer have to rely on the array structure but just call the public methods likeProjectRelease::getDownloadUrl()We can't do much about the fact that
update_calculate_project_update_status()follows the API-by-array pattern. It is public function so we can't just remove it or change it's signature. At some point we should probably deprecate it but I am not sure we can do that before Drupal 10. We can however change it internally to as much as possible to just alter the array by calling public methods on well scoped classes. This what I am trying to do in the merge request. I am not sure if we need to do it all in 1 issue.Back to @dww suggestions in #21
$project_data['status']as this is set in the currently this is bit tricky to replace completely withProjectStatusCalculator::getStatus()by doing something like$project_data['status'] = $status_calculator->getStatus();Because for instance
$project_data['status'] = UpdateManagerInterface::NOT_SUPPORTEDappears multiple times. Once it is stores
project_statusfrom the XML, another time it is because the installed version is not in a supported branch and another time it is because installed version is marked unsupported.I don't think we want to cover all those cases in
getStatus()of the new class because the method would give no indication of how whether that status reflects project status on Drupal.org from the XML or is related to the specific release they had have installed.Currently
update_calculate_project_update_status()handles this by in some cases by setting$project_data['extra']and other cases by settings$project_data['reason']to give more context. But these seem very tied to how they will be displayed because they seem to both store the human readable "reason" for that status.One way to handle this would to
notmove the "Reason" and "Extra" logic into the new class in this issue. The current merge request adds another classUpdateServerProjectInfo(open to a better name) we could addUpdateServerProjectInfo::getStatus()and let the logic for reacting to that stay inupdate_calculate_project_update_status()for now.These are covered by the new public methods on
ProjectStatusCalculator:getLatestReleaseForTargetMajor(), getRecommendReleaseForTargetMajor(), getReleasesInMajorsGreaterThanTarget()currently "also"getDevReleaseForTargetMajor()andgetLatestDev()getSecurityReleases()Comment #34
tedbowComment #35
tedbowComment #36
tedbowComment #38
tedbowTagging with "Automatic Updates Initiative" because this would make it much use to write a new method like
getRecommendReleaseInTargetMinor()Comment #39
phenaproximaHiding the patches in favor of the merge request.
Comment #41
tedbowComment #45
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It either no longer applies to Drupal core, or fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".
Apart from a re-roll or rebase, this issue may need more work to address feedback in the issue or MR comments. To progress an issue, incorporate this feedback as part of the process of updating the issue. This helps other contributors to know what is outstanding.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #46
volegerNeeds reroll against 10.1.x branch.
Comment #50
quietone commentedGood sign that tests are passing. I've updated the remaining tasks.
Comment #51
quietone commentedComment #54
smustgrave commentedLeft some comments but looking at the issue summary should the class be called ProjectStatusCalculator?
Comment #55
quietone commentedRebased the older MR, 1761, to 11.x and merged MR 4711. Then closed MR 4711.
Comment #58
quietone commentedComment #59
quietone commentedFound an earlier issue which creates an UpdateParser servicer with a method calculateProjectUpdateStatus that replaces this function. #2167779: Break up Update system classes. Has that approach been considered?
Changing status to get answers to the this question.
Comment #60
quietone commentedIt is understandable why it would make some things easier if this was done before #2990476: If the site is on an insecure version of an old minor and there is a secure version of that old minor available, the update status report should link that release, or any issue that touches update_calculate_project_update_status(). However, that issue is a Critical bug and that should not wait on an architectural change. That bug was reported in 2018, 16 months before this issue was opened and judging by the comments more discussion is needed here.
I've updated the Issue Summary accordingly.
Comment #61
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".
This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #62
quietone commentedComment #63
volegerMR !1761 needs rebase
Comment #65
nicxvan commented