Uploaded image for project: 'Moodle'
  1. Moodle
  2. MDL-80438

Consider adding Laravel-like Facades to core

XMLWordPrintable

    • Icon: Improvement Improvement
    • Resolution: Won't Fix
    • Icon: Minor Minor
    • None
    • 4.4
    • General
    • None

      A feature we discussed in MDL-80072 was the inclusion of Facades in Moodle core.

      Facades in the Laravel context make it easier and more natural to use DI-like features.

      Rather than querying the DI for a class:

      \core\di::get(\core\formatting::class)->format_text(...$args);
      

      You can use a 'facade' to to the fetch from DI and call the method:

      use \core\facade\formatting;
       
      formatting::format_text(...$args);
      

      A facade is a very thin layer which just makes use of the callStatic magic method to call the original via an identifier in the facade itself.

      The abstract facade class therefore looks something like this:

      abstract class facade {
          abstract public static function get_facade_accessor(): string;
       
          public static function __callStatic($method, $args) {
              $instance = static::get_facade_instance();
       
              if (!$instance) {
                  throw new RuntimeException('Unable to find a facade object.');
              }
       
              return $instance->$method(...$args);
          }
       
          public static function get_facade_instance() {
              return \core\di::get(static::get_facade_accessor());
          }
      }
      

      And the facade implementation is even simpler:

      namespace core\facade;
       
      class formatting extends \core\facade {
          public static function get_facade_accessor(): string {
              return \core\formatting::class;
          }
      }
      

      When a call is made against the facade, for example \core\facade\formatting::format_text() that is handled by the magic method, whic uses get_facade_instance() to get the concrete class (using DI in our case) with the static::get_facade_accessor() method. It then calls the requested function. So in summary it looks a bit like:

          public static function __callStatic($method, $args) {
              $classname = static::get_facade_accessor();
              $instance = \core\di::get($classname);
       
              if (!$instance) {
                  throw new RuntimeException('Unable to find a facade object.');
              }
       
              return $instance->$method(...$args);
          }
      

      This means that it is still very easy to mock classes by simply injecting them into the DI.

      It is also possible to work with an interface here because DI uses arbitrary string identifiers rather than specific class names. So we could have an interface like \core\string_manager and, rather than using the current approach of using get_string_manager, you instead create a simple facade and call directly on it:

      use core\facade\string_manager;
       
      string_manager::get_string(...$args);
      

      The string manager is then defined in the DI layer instead:

              $builder->addDefinitions([
                  // The string manager.
                  \core\string_manager::class => function() {
                      // The previous content of `get_string_manager()` goes here.
                  },
              ]);
      

      This is, generally speaking, a very natural approach. It does, however, have one major limitation... IDEs.

      Because we are using _callStatic rather than calling an actual static method, we don't have any _real static methods to fetch documentation for in an IDE. Likewise autocompletion and code introspection does not work without a little effort.

      This can be partially solved with adding appropriate php docs to the facade class - specifically the @method static [returntype] [methodname](<argtype> <$argname>,...) syntax.

      Laravel goes one step further and have extensions for tooling like PHPStorm which (I believe) redirects to the actual class/interface and gets the documentation from there instead.

      I have written some basic CLI tooling to take a facade and generate all of the documentation for it.

            dobedobedoh Andrew Lyons
            dobedobedoh Andrew Lyons
            Votes:
            1 Vote for this issue
            Watchers:
            17 Start watching this issue

              Created:
              Updated:
              Resolved:

                Error rendering 'clockify-timesheets-time-tracking-reports:timer-sidebar'. Please contact your Jira administrators.