Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make vendor package classes being extendable with PSR-4 autoloading?

I'm using Laravel as framework to build an application framework which can be used to build their own application by fellow developers. Now I'm running into a little PSR-4 namespacing problem in the composer packages I'm developing.

Example of how my application framework is designed

  • app
    • MyApplicationName
      • Mapper
        • BaseMapper.php (namespace: App\MyApplicationName\Mapper)
  • bootstrap
  • config
  • ...
  • vendor
    • MyVendorName
      • MyApplicationName
        • src
          • Controller
            • BaseController.php
          • Mapper
            • BaseMapper.php (namespace: MyVendorName\MyApplicationName\Controller)

This works correctly. I can make the BaseMapper.php in the app folder extend the BaseMapper.php in vendor and in that way I can add extra methods and properties to the BaseMapper.php while keeping (or overwriting) those in the base mapper file.

Contents of BaseMapper.php in app:

namespace App\MyApplicationName\Mapper;

use MyVendorName\MyApplicationName\Mapper as FrameworkMapper;

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

Contents of BaseMapper.php in vendor:

namespace MyVendorName\MyApplicationName\Mapper;

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

When I call the mapper from within the Laravel project like:

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

I get bar as a result and I'm very happy.

But here's my problem: In the BaseController.php in vendor I call a BaseMapper.php's method like:

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

Now $result is foo while I expect to get bar.

I can solve it by adding:

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

$result = $mapper->executeFunction();

But I think it's ugly to add checks like that and besides it's easily forgotten to add a check like that when you instantiate a new class.

So I want to add some custom autoloading logic (in my package) to be able to search for the App namespace first and to use the MyVendorName\MyApplicationName namespace as a fallback.

I've already tried to do this in composer.json:

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

But this make it impossible to overwrite the class from within the app folder as I'm using the same namespace.

So my only idea to achieve this is when there's a way to hook in on the autoloader of composer to add logic like this:

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

Could you please help me out how to achieve this giving me another (better) solution for my problem?

like image 560
Erwin Augustijn Avatar asked Sep 08 '25 05:09

Erwin Augustijn


1 Answers

This should not be addressed with a custom autoloader or swapping out classes at runtime. Ideally, vendor/MyVendorName/MyApplicationName/Controller/BaseController would be written such that it could have the BaseMapper classed injected at instantiation, so that you could very easily change its behavior simply by passing it the class it needs.

namespace MyVendorName\MyApplicationName\Controller;

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

Then when you create the controller class, you'd pass it the mapper you wanted it to use:

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

However, if you don't have the ability or the desire to change that vendor repository separately, then the simplest solution would probably be to create an extending BaseController in your app namespace so that it extends the vendor BaseController. Then provide an overriding method that has the updated logic:

Create 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();
    }
}

And then in your app, use the new controller:

//$controller = new \MyVendorName\MyApplicationName\Controller\BaseController();
$controller = new \MyApplicationName\Controller\BaseController();
like image 61
Alex Howansky Avatar answered Sep 10 '25 10:09

Alex Howansky