TLDR

The Drupal\Core\Extension\Extension uses magic __call() to pretend that it is also the extension file.
This muddles concerns that should be separate, and makes the class more complex than it needs to be.

An extension is not conceptually identical with its *.info.yml file.

Current situation

The class Drupal\Core\Extension\Extension was introduced in #2188661: Extension System, Part II: ExtensionDiscovery.
It contains one method Extension::__call(), which re-routes calls to SplFileInfo.

  /**
   * Re-routes method calls to SplFileInfo.
   *
   * Offers all SplFileInfo methods to consumers; e.g., $extension->getMTime().
   */
  public function __call($method, array $args) {
    if (!isset($this->splFileInfo)) {
      $this->splFileInfo = new \SplFileInfo($this->pathname);
    }
    return call_user_func_array([$this->splFileInfo, $method], $args);
  }

I claim that this is a bad idea.

As a reader, one immediately wonders: Which parts of \SplFileInfo do we actually want to expose?
How can I find usages of those methods? The IDE won't help, because it's all magic.

We could declare the implicit methods as @method tags.
I did this locally, and my IDE tells me that 2 of those are actually used, 22 are not used, and 4 are overwritten by an explicit implementation.


namespace Drupal\Core\Extension;

/**
 * Defines an extension (file) object.
 *
 * Used:
 * @method getMTime()
 * @method getCTime()
 *
 * Overwritten by explicit method (IDE complains):
 * @method getPath()
 * @method getFilename()
 * @method getPathname()
 * @method getType() - meaning changed.
 *
 * Not used:
 * @method getExtension()
 * @method getBasename()
 * @method getPerms()
 * @method getInode()
 * @method getSize()
 * @method getOwner()
 * @method getGroup()
 * @method getATime()
 * @method isWritable()
 * @method isReadable()
 * @method isExecutable()
 * @method isFile()
 * @method isDir()
 * @method isLink()
 * @method getLinkTarget()
 * @method getRealPath()
 * @method getFileInfo()
 * @method getPathInfo()
 * @method openFile()
 * @method setFileClass()
 * @method setInfoClass()
 * @method __toString()
 */
class Extension implements \Serializable {

Problems

The added magic method Extension::__call() adds potential complexity to the class, only so that a few places of consumer code can get the mtime and the ctime of the file.

It also pretends that the extension is identical with the file, which it shouldn't be.

It also introduces a huge BC commitment, if we consider the entire SplFileInfo as part of the public API of the class Extension.

I am sure there were reasons for this, but that doesn't mean it was a good idea, or inevitable.

Solution

- Add a method Extension::getFileInfo() to get the SplFileInfo of the *.info.yml file.
- Deprecate the magic Extension::__call() for removal in Drupal 11

This won't require major changes in core, and contrib could still work as it is for now.

Comments

donquixote created an issue. See original summary.

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

andypost’s picture

Version: 10.1.x-dev » 10.0.x-dev
Category: Task » Bug report

Now this method caused phpstan to fail so changing to bug, see https://www.drupal.org/pift-ci-job/2520505 and #3015812-112: Introduce new Theme extension object and properly deprecate REGIONS_VISIBLE and REGIONS_ALL

Solution could be add annotation as the issue doing or provide some method to return iterator

andypost’s picture

The usage is not that big

$ git grep '__call('
core/lib/Drupal/Component/Annotation/Plugin/Discovery/AnnotationBridgeDecorator.php:71:  public function __call($method, $args) {
core/lib/Drupal/Component/Datetime/DateTimePlus.php:351:  public function __call($method, array $args) {
core/lib/Drupal/Component/Plugin/Discovery/DerivativeDiscoveryDecorator.php:250:  public function __call($method, $args) {
core/lib/Drupal/Component/Plugin/Discovery/StaticDiscoveryDecorator.php:63:  public function __call($method, $args) {
core/lib/Drupal/Core/Database/Query/SelectExtender.php:512:  public function __call($method, $args) {
core/lib/Drupal/Core/Extension/Extension.php:166:  public function __call($method, array $args) {
core/lib/Drupal/Core/Plugin/Discovery/InfoHookDecorator.php:59:  public function __call($method, $args) {
core/lib/Drupal/Core/Plugin/Discovery/YamlDiscoveryDecorator.php:49:  public function __call($method, $args) {
core/lib/Drupal/Core/Routing/AccessAwareRouter.php:61:  public function __call($name, $arguments) {
core/modules/migrate/src/Plugin/Discovery/ProviderFilterDecorator.php:95:  public function __call($method, array $args) {
core/modules/migrate/src/Plugin/NoSourcePluginDecorator.php:54:  public function __call($method, array $args) {
core/modules/views_ui/src/ViewUI.php:905:  public function __call($method, $args) {
core/tests/Drupal/Tests/Component/Plugin/Discovery/StaticDiscoveryDecoratorTest.php:186:   *     mocked __call() will return them.
geek-merlin’s picture

We have a well-proven deprecation workflow, so +1 for the clean solution option 1, to add getSplFileInfo (we can bikeshed on the method name) and deprecate __call.

andypost’s picture

Status: Active » Needs review
StatusFileSize
new2.63 KB

Let's see if there's a any other calls to it

Status: Needs review » Needs work

The last submitted patch, 14: 2959989-14.patch, failed testing. View results

andypost’s picture

Version: 10.0.x-dev » 10.1.x-dev
Status: Needs work » Needs review
StatusFileSize
new939 bytes
new939 bytes

and debug information

andypost’s picture

StatusFileSize
new2.64 KB

valid patch

Status: Needs review » Needs work

The last submitted patch, 17: 2959989-17.patch, failed testing. View results

andypost’s picture

Status: Needs work » Needs review
StatusFileSize
new1.62 KB
new3.33 KB

So only

  • getMTime() used once in runtime and once in api docs
  • getCTime() used once in runtime
andypost’s picture

  1. +++ b/core/lib/Drupal/Core/Extension/ExtensionList.php
    @@ -555,7 +555,7 @@ protected function createExtensionInfo(Extension $extension) {
    +    $info['mtime'] = $extension->getFileInfo()->getMTime();
    

    we can deprecate this key as it is not parsed see \Drupal\Core\Utility\ProjectInfo::filterProjectInfo() and used only by tests

  2. +++ b/core/lib/Drupal/Core/Utility/ProjectInfo.php
    @@ -81,7 +81,7 @@ public function processInfoList(array &$projects, array $list, $project_type, $s
    +        $file->info['_info_file_ctime'] = $file->getFileInfo()->getCTime();
    

    consumed only by locale and update modules

andypost’s picture

geek-merlin’s picture

Title: Was Extension::__call() magic method a good idea? » Deplrecate Extension::__call() magic
Status: Needs review » Reviewed & tested by the community
Related issues: +#3322609: Deprecate $extensionInfo['mtime'] and ['_info_file_ctime']

Hey, that ended up simple!

Code looks straightforward and tests are green.
Added a draft CR.
Added followup for #20.

Deeming RTBC.

andypost’s picture

Status: Reviewed & tested by the community » Needs work

Needs to fix link to real cr and add tests somehow

avpaderno’s picture

Title: Deplrecate Extension::__call() magic » Deprecate Extension::__call() magic
andypost’s picture

Status: Needs work » Needs review
StatusFileSize
new5.17 KB
new2.77 KB

Fixed link to CR and added deprecation test, now looks ready

IS needs to be fixed because I'm using more generic name getFileInfo() instead of mentioning implementation details of SPL (which could be a cause of naming issue for caps/no-caps)

spokje’s picture

Status: Needs review » Needs work

Sad TestBot :(

andypost’s picture

Status: Needs work » Needs review
StatusFileSize
new944 bytes
new4.74 KB

fix c/p error

feuerwagen’s picture

Issue summary: View changes
Status: Needs review » Reviewed & tested by the community

Testbot is green, test looks good. Also updated the IS.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/lib/Drupal/Core/Extension/Extension.php
@@ -160,12 +160,25 @@ public function load() {
+  /**
+   * Returns SplFileInfo instance for the extension's info file.
+   */
+  public function getFileInfo(): \SplFileInfo {

Missing @return documentation. I think we should still have this - unless we've changed our coding standards.

andypost’s picture

Status: Needs work » Needs review
StatusFileSize
new642 bytes
new4.9 KB

Thanks, added

alexpott’s picture

Status: Needs review » Reviewed & tested by the community

Marking as RTBC as the only changes here are comments. Thanks @andypost.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed 5b8e018 and pushed to 10.1.x. Thanks!

  • alexpott committed 5b8e018 on 10.1.x
    Issue #2959989 by andypost: Deprecate Extension::__call() magic
    

Status: Fixed » Closed (fixed)

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