I use some commerce demo shop which happens to have a custom binary path set (in composer.json) alongside the vendor directory (and not like default in vendor/bin)

composer.json:

"config": {
    "bin-dir": "bin",
    "sort-packages": true
},

In the admin UI this fails unfortunately silently and the check passes and I got fooled into thinking I'm a hero and all is good. But when I checked watchdog there is an error logged:

PHPStan executable not found.

Running drush upgrade_status:analyze yourmodule the error is also shown (and more obvious) but still finishes with "No known issues found":

drush upgrade_status:analyze commerce_btcpay
[error] PHPStan executable not found.
[notice] Processing /path/to/drupal/web/modules/custom/yourmodule.
sh: /bin/phpstan: No such file or directory
[warning] Invalid argument supplied for foreach() DeprecationAnalyzer.php:252

================================================================================
Your Module, 8.x-1.0-alpha3
Scanned on Mon, 04/20/2020 - 21:31

No known issues found.

Findings after some debugging:

DeprecationAnalyzer.php in findVendorPath()

  • assumes that bin directory is always located inside vendor
  • checks for phpstan binary (so function name is confusing or should only check if vendor exists?)
  • does not throw an exception and does not exit (only issues an error message not visible in the UI, and does not return NULL like suggested)
  • in constructor, after calling findVendorPath() there is no check if a path or null was returned, no stop of further execution

Proposed solution:

  • findVendorPath(): detect only vendor path (not phpstan binary)
  • findVendorPath(): bail and throw exception if not found (makes no sense to continue if vendor is missing/not found?)
  • introduce separate method findBinPath()
  • findBinPath(): read composer.json config bin-dir (not sure what the best approach is here, read the main composer.json like you do for contrib modules?)

The patch is a rough quick fix which does not check above suggested composer.json for bin-dir config yet but uses a copy of findVendorPath() for now (but including it anyway for a first look).

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

ndeet created an issue. See original summary.

ndeet’s picture

Issue summary: View changes
FileSize
3.15 KB
Gábor Hojtsy’s picture

Status: Needs work » Needs review

Sending this for a test. I don't honestly know how to find the site's composer.json file. Is it where the global bin dir would then be defined?

Gábor Hojtsy’s picture

I think #3126174: Warning: Invalid argument supplied for foreach() was related which I think was likely also due to a missing phpstan. We don't do a very good job of letting people know if phpstan was not found.

Gábor Hojtsy’s picture

Title: Custom binary path fails silently » When phpstan is not found, the error is too subtle, does not support custom binary path

Making title more explicit. Fixing the too subtle error would be really useful for everyone.

ndeet’s picture

Re finding bin-dir config:

Option 1: read main composer.json
I saw this code how contrib modules composer.json is currently read and evaluated and thought this would be a good approach for the projects main composer.json too eventually?
https://git.drupalcode.org/project/upgrade_status/-/blob/8.x-2.x/src/Dep...

First reuse findVendorPath() to get the right vendor path in docroot or DRUPAL_ROOT e.g. dirname($this->findVendorPath()) . '/composer.json'
then use the above mentioned approach to get the config via $composer_json->config->{'bin-dir'} or something like that?

Option 2: getting it from a Composer class? (preferred)
I do not know if there is a way to use a Composer class and get to that information but it seems \Composer\Config is not available. Maybe somebody has an idea for that, I did not find something searching but could have missed something obvious.

Gábor Hojtsy credited fgm.

Gábor Hojtsy’s picture

Status: Needs review » Needs work

@fgm also suggested hardening right where we get the json results from phpstan, which would also be invalid if the file is not there or we cannot run it or there was some other error with it. See interdiff in #3129903-26: Remove git deploy as a requirement from upgrade status. I did not commit those there because it was unrelated. Incorporating that here would be great.

Re how do we read composer information, following #3129903: Remove git deploy as a requirement from upgrade status we don't rely on git_deploy anymore and suggest either composer_deploy or git_deploy for more accurate categorisation but have some basic heuristics ourselves. It is probably worth a look at how composer_deploy finds the composer information because going along those lines would probably be the most proven so far :)

eojthebrave’s picture

I just ran into this same issue.

We could use weblo/drupal-finder to locate the root composer.json file. The project is already required by mglaman/phpstan-drupal which this project depends on so we could make it an explicit dependency and then do something like:

$finder = new DrupalFinder\DrupalFinder();
$finder->locateRoot(DRUPAL_ROOT);
$composerRootFile = $finder->getComposerRoot() . '/' . $finder->getComposerFileName();

Then read the contents of the file and figure out what the bin directory setting is.

eojthebrave’s picture

Status: Needs work » Needs review
FileSize
4.46 KB

Here's a patch that uses webflo/drupal-finder.

I wasn't sure what additional code you wanted added in #8. It looks like the referenced code in the interdiff was already added. But, if you can provide a little more direction as to what you're looking for I can try and update this patch as needed.

Status: Needs review » Needs work

The last submitted patch, 10: 3129430-10-allow-custom-bin-dir.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

eojthebrave’s picture

Status: Needs work » Needs review
FileSize
4.3 KB

Oops, that's not going to work. I removed the findVendorPath() function, but then didn't remove uses of it. This patch should actually work. :)

Gábor Hojtsy’s picture

Re #8 I did notice I commit it already. That said it still just more silently swallows the error if phpstan did not return a JSON. If it was not executable (permission issues or got removed or bad bin path or whatnot) then it will just behave like there was no error and everyone is happy :) we should log an error into the project's error list (not to a logger channel IMHO because if you got green results you will not think to go check your Drupal logs for errors).

Gábor Hojtsy’s picture

FileSize
4.94 KB
2.02 KB

This is starting to look great, thanks!

+++ b/src/DeprecationAnalyzer.php
@@ -150,22 +169,41 @@ final class DeprecationAnalyzer {
+    if (is_null($json)) {
+      throw new \Exception('Unable to decode ' . $rootComposer);
     }
...
+    // Bail here as continuing makes no sense.
+    throw new \Exception('Binary path not found.');

I notice we don't catch these and make it visible to users that it did not work. So it will fail in some ugly way for users on the UI. I did not resolve this yet, as we need to figure out how to resolve this.

The second exception thrown does not explain what's binary path is not found either. I added some code for that and also to list what we tried in the message which should help a lot in debugging.

I also added logging code from @DamienMcKenna from #3132908: Do not fail entire scan if drupal.org cannot be reached to log errors when phpstan did not return a valid result (which may also be because there was no permission to run it or it was moved from there or some other massive fail).

Setting needs review for testbot sake but actually needs work for exposing the problem to the user rather than failing out of an exception entirely.

Gábor Hojtsy’s picture

Issue summary: View changes
FileSize
131.39 KB
12.01 KB
8.83 KB

Ok I took a stab at surfacing the issues to the user. So I separated the environment specific parts of the constructor to its own method, which can now freely throw exceptions right and left :D Made the existing error conditions for temporary directories, etc. exceptions as well for consistency. Then made analyze() call that but return silently if there was a problem, since we now assume the integration code invokes it first and reports any environment errors first before any analyzation would take place. Then added a bit of a dsm() to the form, so the error appears to the user and disabled the form since the error needs fixing first. I also looked into disabling the checkboxes but #2895352: Allow tableselect element options to be disabled is still outstanding for that and I did not want to do all that workaround to disable them.

Here is how it look like now, which I think is good :)

  • Gábor Hojtsy committed 078e6b5 on 8.x-2.x
    Issue #3129430 by Gábor Hojtsy, eojthebrave, ndeet, fgm, DamienMcKenna:...

  • Gábor Hojtsy committed 4356a5c on 8.x-2.x
    Issue #3129430 followup by Gábor Hojtsy: add a hint to phpstan finder...
Gábor Hojtsy’s picture

Status: Needs review » Fixed

Tests also like it, so let's get this in and improve further later as needed. Thanks all for working on this! It will make life much easier for people setting up the module. It will also catch mistakes like not installing the module with composer and thus not having phpstan installed at all. Running phpstan will not be attempted in this case and the failure will be surfaced to the user. (I already did a quick followup commit here to expand on the error message in the screenshot and add a Did you install Upgrade Status with composer? hint to the error message displayed.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.