Problem/Motivation

From the originating issue #1002080: [Policy, no patch] Normalize on usage of is_callable() instead of function_exists()

Instead of using function_exists() to verify if a callback exists, Drupal could use is_callable(); this would allow to use object and class methods as callbacks.

Steps to reproduce

N/A

Proposed resolution

Review instances of function_exists.
There are several options:

  • If it is called on a string we know it's a function and we can address it when we convert the function itself.
  • If it is called on a generated string from files other than .module (e.g. $module . '_install') we can convert that when we handle .install, .theme, .engine, .post_update files
  • If it is accepting something like a form callback we can safely convert that to is_callable here.
  • If it is a generated string inside .module files (e.g. $function = $entity->getEntityTypeId() . '_last_viewed';) address here or in follow up
  • If it is something else (e.g. EntityResolverManager: if (function_exists($controller)) {) address here or in follow up.

HandlerBase #3539917: Deprecate access callback in HandlerBase -> already is defined in an array as access callback, function_exists only needs to be changed to CallbackResolver
form.inc #3539919: Convert batch callbacks to CallableResolver -> already is defined in an array key as batch redirect, function_exists only needs to be changed to CallbackResolver
CssOptimizerUnitTest #3539921: Clean up CSSOptimizer unit test. remove extra namespace.
EntityResolverManager #3539922: Deprecate EntityResolverManager procedural controllers deprecate this option - remove procedural callbacks from provider since only one provider option has a deprecation.
CommentManager #3536209: Deprecate magic ENTITY_TYPE_last_viewed functions -> Needs a way to define the class and method. This was deprecated and moved to history, which was removed from core.
ViewsUI #3536204: Deprecate magic cancel submit handlers in Views UI -> Needs a way to define the class and method.

Remaining tasks

Confirm evaluation

User interface changes

N/A

Introduced terminology

N/A

API changes

N/A

Data model changes

N/A

Release notes snippet

N/A

CommentFileSizeAuthor
#18 functionExistsCallsLine.txt11.68 KBnicxvan

Issue fork drupal-3520416

Command icon 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

quietone created an issue. See original summary.

annmarysruthy’s picture

Assigned: Unassigned » annmarysruthy
annmarysruthy’s picture

Assigned: annmarysruthy » Unassigned
annmarysruthy’s picture

@quietone Should we replace all existences of function_exists() with is_callable() ?

quietone’s picture

Priority: Normal » Minor
Issue summary: View changes

@annmarysruthy, I think each usage needs to be examined. For example, the usages in tests should probably not change, In may turn out that there are no places where making this change is an improvement. I have not looked closely at all the instances.

annmarysruthy’s picture

Issue summary: View changes
Priority: Minor » Normal
Status: Active » Needs review

Checked all occurrences of function_exists() and changed 6 files. In my opinion, Rest of the occurrences are better kept as function_exists() itself. Kindly review MR

annmarysruthy’s picture

Priority: Normal » Minor
nicxvan’s picture

This looks great! I have to review more closely, but this already covers the three I was reviewing.

andypost’s picture

curious to compare performance of both methods

nicxvan’s picture

I'd take another look at EntityResolverManager.

nicxvan’s picture

Found some more hooks to exclude.

nicxvan’s picture

Status: Needs review » Needs work

For 11.

nicxvan’s picture

Issue summary: View changes

Ok I am going through this and here are the criteria

nicxvan’s picture

Issue summary: View changes
alexpott’s picture

I don't think we should be changing the string ones here. It's pointless and I think should be address in the issue that does the change.

nicxvan’s picture

I agree, that is the first bullet point, sorry if that wasn't clear.

If it is called on a string we know it's a function and we can address it when we convert the function itself

nicxvan’s picture

StatusFileSize
new11.68 KB

Ok I did the evaluation, it could use confirmation, I've attached it.

I ran this command to get a list of function_exists calls
grep -rn "function_exists" core > functionExistsCallsLine.txt

I then removed comments and broke it into three categories:

  • Actionable (here or in a follow up)
  • Procedural hooks, update, install, legacy invoke or theme related
  • strings or library functions like is_file

Here is the list of actionable ones, I wasn't sure about the css test.
Category 1 Actionable
core/modules/views/src/Plugin/views/HandlerBase.php:572: if (isset($this->definition['access callback']) && function_exists($this->definition['access callback'])) {
core/includes/form.inc:955: if (($function = $batch['redirect_callback']) && function_exists($function)) {
core/modules/comment/src/CommentManager.php:210: if (function_exists($function)) {
core/modules/views_ui/src/ViewUI.php:341: $cancel_submit = function_exists($form_id . '_cancel') ? $form_id . '_cancel' : [$this, 'standardCancel'];
core/tests/Drupal/Tests/Core/Asset/CssOptimizerUnitTest.php:298:if (!function_exists('Drupal\Core\Asset\file_uri_scheme')) {
core/lib/Drupal/Core/Entity/EntityResolverManager.php:93: if (function_exists($controller)) {

nicxvan’s picture

I'm going to work on this.

nicxvan’s picture

Issue summary: View changes
nicxvan’s picture

Issue summary: View changes
nicxvan’s picture

Issue summary: View changes
nicxvan’s picture

Title: Convert function_exists() to is_callable() » Convert function_exists() that still require .module files to is_callable()
Issue summary: View changes

Reducing scope here to just functions that have to be in a .module file.

nicxvan’s picture

Issue summary: View changes
nicxvan’s picture

Issue summary: View changes

nicxvan changed the visibility of the branch 11.x to hidden.

nicxvan’s picture

I created a new MR removing the function_exists call in EntityManagerResolver so we can see what fails.

I accidentally pushed to the other branch so I force pushed that branch to reset it to the state annmarysruthy left it in.

mstrelan’s picture

Create follow up for CommentManager -> Needs a way to define the class and method.

#3536209: Deprecate magic ENTITY_TYPE_last_viewed functions

Create follow up for ViewsUI -> Needs a way to define the class and method.

#3536204: Deprecate magic cancel submit handlers in Views UI

CssOptimizerUnitTest No need to address afaict

We can remove that whole extra namespace at the bottom of the test, since #3034072: Move file uri/scheme functions from file.inc and FileSystem to StreamWrapperManager. Do you want another follow up or should it happen here?

EntityResolverManager does this just need deprecation? what situation can that be a function?

I think the failing test explains this one. I think it can stay, or update to is_callable.

berdir’s picture

Mixed feelings on this. I think almost all registered callbacks should use CallableResolver, for example the batch stuff, because then we could actually use services. I'm also not sure why we even check if for example the redirect_callback. If that's incorrectly defined then it just silently ignores it. As a developer, I'd much prefer to just get an exception/error on that.

For me the question is whether it's really worth changing to is_callable(), only to then either deprecate the whole thing or having it change it again. If we're going to deprecate something, we could just do it straight away? And for batch I'd prefer a dedicated issue to make everything use CallableResolver.

> I think the failing test explains this one. I think it can stay, or update to is_callable.

Well, it's a unit test testing the code, it was most likely written to explicitly cover every line of code. It's existence isn't necessarily proof that it's actually needed. I'd try removing the unit test to see if there's anything else. Unlike others, this is an explicit fallback. It can't be an array, and at this point, it can't really be anything else but a function. So an is_callable() check there seems pointless, either we keep it or we remove it..

kim.pepper’s picture

Agree with @berdir. Using CallableResolver gives us more options and proper validation of callables.

nicxvan’s picture

Title: Convert function_exists() that still require .module files to is_callable() » Handle calls to functions that still require .module file autoloading.
Issue tags: +Needs framework manager review

Thank you everyone, that is exactly what I was looking for.

We should probably rename this since it's more about handling the last edge cases of functions required to be in .module files.

I'm ok with CallableResolver!

So in summary I think the form and handler we use CallableResolver.

In the test we just remove that namespace.

In the other cases we outright deprecate them in children issues.

This might need framework manager review.

nicxvan’s picture

I'm going to close @annmarysruthy's branch and hide it since we have a slightly different strategy now.

In my new branch I will use CallableResolver for form, and Handler.
I will look at updating the test and I will deprecate the function option for EntityResolver.

@berdir I not quite sure I follow this:

I'm also not sure why we even check if for example the redirect_callback. If that's incorrectly defined then it just silently ignores it. As a developer, I'd much prefer to just get an exception/error on that.

Did you mean something like this?

if (($callback = $batch['redirect_callback'])) {
        try {
          $callable_resolver = \Drupal::service(CallableResolver::class);
          $callable = $callable_resolver->getCallableFromDefinition($callback);
          $callable($batch_url->toString(), ['query' => $query_options]);
        } catch (\InvalidArgumentException $e) {
          \Drupal::logger('form')->warning('Batch redirect callback is not valid: %error.', ['%error' => $e->getMessage()]);
        }
      }
      else {
        return new RedirectResponse($batch_url->setAbsolute()->toString(TRUE)->getGeneratedUrl());
      }

nicxvan changed the visibility of the branch 3520416-testEntityResolverManager to hidden.

nicxvan’s picture

Ok I added CallbackResolver to form.inc, removed the extra namespace and deprecated that path in EntityResolverManager.

I'm sure I'll need to clean up tests.

Is there anything special to consider for injecting CallbackResolver in HandlerBase? I see it's using getters and setters for almost everything else, I'm not sure if there is a reason for that.

Also need to handle deprecations being hit in EntityResolverManagerTest

nicxvan’s picture

Issue summary: View changes
nicxvan’s picture

Issue summary: View changes
nicxvan’s picture

Issue summary: View changes
nicxvan’s picture

Title: Handle calls to functions that still require .module file autoloading. » [meta] Handle calls to functions that still require .module file autoloading.

Going to convert this to a meta and open an issue for each.

nicxvan’s picture

Issue summary: View changes
nicxvan’s picture

Issue summary: View changes
nicxvan’s picture

Issue summary: View changes
nicxvan’s picture

Issue summary: View changes
nicxvan’s picture

Issue summary: View changes
quietone’s picture

nicxvan’s picture

Couple more issues in needs review, and one other needs some deprecations in tests tracked down.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.

nicxvan’s picture

Issue summary: View changes
nicxvan’s picture

This is nearly there, just the one issue!

I know usually we close metas once there is only one issue remaining, but I'd like to keep this open a bit.