Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scan for classes based on the PHP interface (WorkflowMessageInterface, ExampleDataInterface) #23854

Merged
merged 7 commits into from
Jun 28, 2022

Conversation

totten
Copy link
Member

@totten totten commented Jun 21, 2022

Overview

Class-scanning allows one to write a class and have it recognized automatically (without any other explicit declarations). There are some existing class-scanners in core; this branch expands that. It is an alternative to #23848. It specifically scans for PHP classes based on the PHP interfaces.

(There should be at least 1 test-failure in the current draft. So far, I have been focused on running the integration-tests rather than r-run. Posting for a more complete test-run.)

Before

There is some precedent for class/interface-based scanning:

  • Civi scans a few folders in civicrm-core for implementations of WorkflowMessageInterface.
  • Civi scans a few folders in civicrm-core for implementations of ExampleDataInterface.
  • Civi scans for BAOs that implement Civi's HookInterface or Symfony's EventDispatcherInterface.

However, each of these has a different scanner with slightly different quirks.

After

The PR provides a generic scanning mechanism. Additionally, it updates existing code to use the generic scanner for specific interfaces (WorkflowMessageInterface, ExampleDataInterface).

Technical Details: Generic mechanism

The helper, Civi\Core\ClassScanner::get(...), finds matching classes.

$classes = ClassScanner::get(['interface' => 'Civi\Foo\BarInterface']);
foreach ($classes as $class) {
  // Do something with $class.
}

Each extension can define classes that implement the interface, eg

// FILE: myextension/CRM/Myextension/Foo.php
class CRM_Myextension_Foo implements \Civi\Foo\BarInterface {
  public function doSomething() {}
}

The scanner requires a general opt-in (per extension). It should be safe for most extensions to opt-in, as it is designed to match existing code-conventions. However, the scanner requirements are slightly stricter. (For example, all PHP files in ./CRM or ./Civi must define eponymous PHP classes. If you have any script-files in those folders, they could be mis-fired by the scanner.) The opt-in requirement ensures that existing extensions will continue to work.

The typical opt-in should go through info.xml:

<!-- FILE: myextension/info.xml -->
<mixins>
  <mixin>scan-classes@1.0.0</mixin>
</mixins>

Alternatively, as a last resort (if you need to support an uncommon file-layout), there is an experimental hook:

function myextension_civicrm_scanClasses(array &$classes) {
  $classes[] = 'CRM_Myextension_Foo';
  $classes[] = 'CRM_Myextension_Bar';
}

Technical Details: Specific interfaces

  • It scans the extensions for classes that implement WorkflowMessageInterface (in addition to scanning the previous core folders).
  • It scans the extensions for classes that implement ExampleDataInterface (in addition to scanning the previous core folders).
  • It scans the extensions for classes that implement HookInterface (in addition to the previous BAO). (*This was included in the original draft. It generally works in practice, but it needs work to address an edge-case in phpunit testing, and it should be updated to support Symfony-style subscribers.)

FAQ

Q: What about Doctrine annotations? What about PHP attributes? What about YAML files? Should we register components with these?

They all have nice properties. But they all create dependency-hell. PHP interfaces are simple, inspect-able, and compatible with all supported environments.

Q: What are the performance characteristics? How will adapt to large source-trees?

There are a couple expenses to look out for: the cost of walking the source-tree, and the cumulative size of the class-list. There are some guard-rails that should keep each of these moderate:

  • It only scans active extensions which enable scan-classes@1.0. It does not scan external PHP libraries or CMS plugins.
  • It targets the common class-folders (./CRM and ./Civi). It ignores other folders (like ./vendor, ./templates, or ./css).
  • It performs one scan, regardless of how many interfaces or extensions are used.
  • It only cares for interfaces in the CRM and Civi namespaces. (Other interfaces are ignored.)
  • It caches the outcome, storing separate indices for each interface. (Ex: There is a cache-record for WorkflowMessageInterface with a list of matching classes. When enumerating WorkflowMessageInterface, it will load this list. It doesn't load the full class-list.)

There are limits, of course. If you define an interface that is both very broad (eg used in a huge number of classes) and very active (eg used in most page-loads), then the cumulative effect would a huge number of class-loads during a most page-loads. So... don't do that.

@civibot
Copy link

civibot bot commented Jun 21, 2022

(Standard links)

@civibot civibot bot added the master label Jun 21, 2022
@totten totten force-pushed the master-mixin-wfmsg branch from 7da39b9 to 9936dce Compare June 21, 2022 14:04
@colemanw
Copy link
Member

This seems cool. What about EventSubscriberInterface? That would be a good one to support.

@totten totten force-pushed the master-mixin-wfmsg branch 3 times, most recently from 7c24407 to 3ac5c0f Compare June 24, 2022 06:35
@totten
Copy link
Member Author

totten commented Jun 24, 2022

This seems cool. What about EventSubscriberInterface? That would be a good one to support.

We discussed this a bit more the other day. Outcome: It should basically use a copy of EventSubscriberInterface, because (1) it is a useful pattern, (2) other packages may use EventSubscriberInterface in different ways, where auto-registration is problematic and (3) we should have a buffer against flipflopping in Symfony's definition of the interface.

I'm going to split out the hook/event auto-subscriber stuff as a separate PR because (1) this PR is already big enough and (2) the hook/event stuff needs some work to address a phpunit issue.

@totten totten changed the title (EXP) Add support for loading classes based on the PHP interface Scan for classes based on the PHP interface (WorkflowMessageInterface, ExampleDataInterface) Jun 24, 2022
@eileenmcnaughton
Copy link
Contributor

@totten I think this might need to be against the rc as we actually need to get something into 5.51 to handle this functionality for import parsers to fix extensions to work with it

@eileenmcnaughton
Copy link
Contributor

In terms of the code - it looks pretty good - I am going to test it out against the rc in our WMF config later today

@totten
Copy link
Member Author

totten commented Jun 27, 2022

I think this might need to be against the rc as we actually need to get something into 5.51 to handle this functionality for import parsers to fix extensions to work with it

OK, it should be easy for me to rebase to 5.51-rc.

As it currently stands, I'm a bit more comfortable with the WorkflowMessageInterface / ExampleDataInterface being exposed that way (because they were not really available downstream before - so it's greener terrain). I'm not sure about #23877, but that's a separate PR.

@totten totten force-pushed the master-mixin-wfmsg branch from 3ac5c0f to 0bfd76b Compare June 28, 2022 00:14
@totten
Copy link
Member Author

totten commented Jun 28, 2022

Split out a small cleanup in #23885. (#23885 is a simple test-only-regression that should be merged ASAP. It is needed on master/5.52 but not on 5.51.)

Rebased on 5.5.1

@totten totten changed the base branch from master to 5.51 June 28, 2022 00:15
@civibot civibot bot added 5.51 and removed master labels Jun 28, 2022
totten added 7 commits June 27, 2022 17:18
…interface-php test.

* The interface-php test registers an instance of WorkflowMessageInterface.
* The list of WorkflowMessageInterface's is stored in the 'long' cache.
* When you enable/disable an extension, it should clear WorkflowMessage list (and other things).
* Before this patch, interface-php's `LifecycleTest` is inconsistent: it passes with
  `testLifecycleWithSubprocesses()` and fails with `testLifecycleWithLocalFunctions()`.
* After this patch, interface-php's `LifecycleTest` is consistent: it passes with both
  `testLifecycleWithSubprocesses()` and  `testLifecycleWithLocalFunctions()`.
* The problem - while executing `testLifecycleWithLocalFunctions()`, it evidentally uses
  `Arraycache` as the backend -- and thus skips the flushes. However, I cannot fathom why
  one would want to clear-caches for SQL+memcache+redis but keep them for Arraycache.
@eileenmcnaughton
Copy link
Contributor

hmm conflict....

@totten totten force-pushed the master-mixin-wfmsg branch from 0bfd76b to 652a831 Compare June 28, 2022 00:21
@totten
Copy link
Member Author

totten commented Jun 28, 2022

Conflict should be resolved now. It was between (a) 5.51-rc commit adding a couple lines to example (691345e) and (b) PR commit to rename example file (d170eb5) and

@eileenmcnaughton
Copy link
Contributor

would it be a cleaner up-merge if we pulled in that commit?

@eileenmcnaughton eileenmcnaughton added the merge ready PR will be merged after a few days if there are no objections label Jun 28, 2022
@eileenmcnaughton
Copy link
Contributor

This works in my testing, allowing loading of templates from extensions with the mixin. Per above it might be cleaner to port the conflicting patches in - but I'm also OK merging as is

@eileenmcnaughton eileenmcnaughton merged commit b03e69b into civicrm:5.51 Jun 28, 2022
@totten totten deleted the master-mixin-wfmsg branch June 28, 2022 03:23
wmfgerrit pushed a commit to wikimedia/wikimedia-fundraising-crm that referenced this pull request Jun 29, 2022
By including civicrm/civicrm-core#23854
in the deploy we can then remove from a civi-hack-patch whereby we
were putting these templates in core.

Note that this should be no change on current master.

With the latest Civi to go out it should pass tests without us needing
https://gerrit.wikimedia.org/r/c/wikimedia/fundraising/crm/civicrm/+/801440
as these files will be loaded from the extension using the very latest
upstream code

Bug: T309250

Change-Id: Ie9094feaf20a5b0b5e5a40628756dc4c5ce94f43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
5.51 has-test merge ready PR will be merged after a few days if there are no objections
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants