【问题标题】:How to make vendor package classes being extendable with PSR-4 autoloading?如何通过 PSR-4 自动加载使供应商包类可扩展?
【发布时间】:2021-11-02 11:16:02
【问题描述】:

我正在使用 Laravel 作为框架来构建一个应用程序框架,该框架可以被其他开发人员用来构建他们自己的应用程序。 现在我在开发的作曲家包中遇到了一点 PSR-4 命名空间问题。

我的应用程序框架的设计示例

  • 应用程序
    • 我的应用程序名称
      • 映射器
        • BaseMapper.php(命名空间:App\MyApplicationName\Mapper
  • 引导
  • 配置
  • ...
  • 供应商
    • 我的供应商名称
      • 我的应用程序名称
          • 控制器
            • BaseController.php
          • 映射器
            • BaseMapper.php(命名空间:MyVendorName\MyApplicationName\Controller

这可以正常工作。我可以使app 文件夹中的BaseMapper.php 扩展vendor 中的BaseMapper.php,这样我可以向BaseMapper.php 添加额外的方法和属性,同时保留(或覆盖)基本映射器文件中的那些.

appBaseMapper.php的内容:

namespace App\MyApplicationName\Mapper;

use MyVendorName\MyApplicationName\Mapper as FrameworkMapper;

class BaseMapper extends FrameworkMapper;
{
    public function executeFunction(): string
    {
        return "bar";
    }
}

BaseMapper.phpvendor 中的内容:

namespace MyVendorName\MyApplicationName\Mapper;

class BaseMapper
{
    public function executeFunction(): string
    {
        return "foo";
    }
}

当我从 Laravel 项目中调用映射器时,例如:

$baseMapper = new \App\MyApplicationName\BaseMapper();
$result = $baseMapper->executeFunction(); // bar

结果我得到了bar,我很高兴。

但这是我的问题: 在vendor 中的BaseController.php 中,我调用了BaseMapper.php 的方法,例如:

$mapper = new \MyVendorName\MyApplicationName\Mapper\BaseMapper();
$result = $mapper->executeFunction(); // foo

现在$resultfoo 而我希望得到bar

我可以通过添加来解决它:

$mapper = new \MyVendorName\MyApplicationName\Mapper\BaseMapper();
if (class_exists(\App\MyApplicationName\Mapper\BaseMapper::class)) {
    $mapper = new \App\MyApplicationName\Mapper\BaseMapper();
}

$result = $mapper->executeFunction();

但我认为添加这样的检查很难看,而且当你实例化一个新类时很容易忘记添加这样的检查。

所以我想添加一些自定义自动加载逻辑(在我的包中),以便能够首先搜索 App 命名空间并将 MyVendorName\MyApplicationName 命名空间用作后备。

我已经在composer.json 中尝试过这样做:

"autoload": {
   "psr-4": {
      "MyVendorName\MyApplicationName": ["../../app/MyApplicationName/","src/"]
   }
}

但这使得无法从 app 文件夹中覆盖类,因为我使用的是相同的命名空间。

所以我实现这一点的唯一想法是当有一种方法可以连接到 composer 的自动加载器以添加如下逻辑:

if (str_contains($className, "\\MyVendorName\\MyApplicationName\\")) {
    $appClassName = str_replace("\\MyVendorName\\MyApplicationName\\", "\\App", $className);
    if (class_exists($appClassName)) {
        include_once $appClassName;
    } else {
        include_once $className;
    }
}

您能否帮我解决如何实现这一点,为我的问题提供另一个(更好的)解决方案?

【问题讨论】:

    标签: php laravel package autoload psr-4


    【解决方案1】:

    不应通过自定义自动加载器或在运行时换出类来解决此问题。理想情况下,vendor/MyVendorName/MyApplicationName/Controller/BaseController 应该这样编写,以便在实例化时注入 BaseMapper 类,这样您就可以非常轻松地更改其行为,只需将其传递给它需要的类。

    namespace MyVendorName\MyApplicationName\Controller;
    
    class BaseController
    {
        protected $mapper;
        public function __construct($mapper)
        {
            $this->mapper = $mapper;
        }
        public function whateverThisMethodIsNamed()
        {
            $result = $this->mapper->executeFunction();
        }
    }
    

    然后,当您创建控制器类时,您会将您希望它使用的映射器传递给它:

    $mapper = new \MyApplicationName\Mapper();
    $controller = new \MyVendorName\MyApplicationName\BaseController($mapper);
    

    但是,如果您没有能力或不想单独更改该供应商存储库,那么最简单的解决方案可能是在您的应用命名空间中创建一个扩展 BaseController,以便它扩展供应商 BaseController .然后提供一个具有更新逻辑的覆盖方法:

    创建app/MyApplicationName/Controller/BaseController.php:

    namespace MyApplicationName\Controller;
    
    class BaseController extends \MyVendorName\MyApplicationName\Controller\BaseController
    {
        public function whateverThisMethodIsNamed()
        {
            $Mapper = new \MyApplicationName\Mapper\BaseMapper();
            $result = $Mapper->executeFunction();
        }
    }
    

    然后在您的应用中,使用新的控制器:

    //$controller = new \MyVendorName\MyApplicationName\Controller\BaseController();
    $controller = new \MyApplicationName\Controller\BaseController();
    

    【讨论】:

      【解决方案2】:

      Alex Howansky 分享了一个非常好的解决方案,但是您正在使用 Laravel,所以使用类似的框架不是一个好的解决方案。

      为什么?你必须使用依赖注入(正如他所提到的)所以使用Laravel's Service Container 来解决这个问题:

      use MyVendorName\MyApplicationName\Contracts\Mapper;
      
      class YourController extends Controller
      {
          public function index(Mapper $mapper)
          {
              $mapper->executeFunction();
          }
      }
      

      所以,你必须创建一个接口,然后将它绑定到你希望它被解析的类,所以当你请求它时(就像在我的代码中一样),它会自动解析为 App 而不是 @987654328 @。

      完成此操作后,您必须将其绑定到您想要的任何服务提供商(我建议使用AppServiceProvider 或创建一个新的)并使用方法register

      namespace App\Providers;
      
      use App\MyApplicationName\Mapper;
      use Illuminate\Support\ServiceProvider;
      use MyVendorName\MyApplicationName\Contracts\Mapper as MapperContract;
      
      class AppServiceProvider extends ServiceProvider
      {
          /**
           * Register any application services.
           *
           * @return void
           */
          public function register()
          {
              $this->app->bind(MapperContract::class, Mapper::class);
          }
      
          /**
           * Bootstrap any application services.
           *
           * @return void
           */
          public function boot()
          {
              //
          }
      }
      

      就是这样!阅读文档以了解更多信息,另请阅读Service Provider 部分,以便您对这个主题有更多了解,因为如果您以前从未遇到过这个主题,它会很复杂。

      最后一件事,我建议创建一个接口,因为这应该是最好的做法,但您也应该能够直接将您想要的 vendor 类替换为您想要的 app 类,但是我我现在(从记忆中)不确定它是否会起作用:

      namespace App\Providers;
      
      use App\MyApplicationName\Mapper;
      use Illuminate\Support\ServiceProvider;
      use MyVendorName\MyApplicationName\Mapper as VendorMapper;
      
      class AppServiceProvider extends ServiceProvider
      {
          /**
           * Register any application services.
           *
           * @return void
           */
          public function register()
          {
              $this->app->bind(VendorMapper::class, Mapper::class);
          }
      
          /**
           * Bootstrap any application services.
           *
           * @return void
           */
          public function boot()
          {
              //
          }
      }
      

      所以,最重要的是你输入你想要的类的提示,所以当任何类使用resolveapp 实例化时,它将使用正确的类。但是,一旦方法开始运行,您也可以使用这两种方法来请求任何类(并且您没有将某些东西作为参数注入,但您在方法运行时要求提供此信息)。

      最后一件事,请阅读Package Development,因为它可能非常复杂,我对创建包没有太多专业知识,但它会对你有所帮助。

      【讨论】:

        猜你喜欢
        • 2015-01-08
        • 2020-01-01
        • 2017-05-05
        • 2015-11-23
        • 1970-01-01
        • 2015-07-31
        • 2015-11-20
        • 2023-01-31
        • 2017-09-15
        相关资源
        最近更新 更多