Reputation: 101
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.
BaseMapper.php
(namespace: App\MyApplicationName\Mapper
)BaseController.php
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?
Upvotes: 4
Views: 298
Reputation: 8082
Alex Howansky shared a really good solution, but you are working with Laravel, so that is not a good solution using a framework like it.
Why ? You have to make use of Dependency Injection (as he mentioned) so use the Laravel's Service Container to resolve this:
use MyVendorName\MyApplicationName\Contracts\Mapper;
class YourController extends Controller
{
public function index(Mapper $mapper)
{
$mapper->executeFunction();
}
}
So, you have to create an interface and then bind it to the class you want it to be resolved, so when you ask for it (like in my code), it will automatically resolve to App
instead of MyVendor
.
Once you have done that, you have to bind it in any service provider you want (I would recommend to use AppServiceProvider
or create a new one) and use the method 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()
{
//
}
}
And that's it ! Read the documentation to understand more, also read the Service Provider
section so you understand more about this topic because it is complex if you have never encountered this before.
One last thing, I am recommending creating an interface because that should be the best thing to do, but you should also be able to directly replace the vendor
class you want with the app
class you want, but I am not sure now (from memory) if it will work:
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()
{
//
}
}
So, the most important thing is that you type hint the class you want, so when any class gets instantiated using resolve
or app
, it will use the correct one. But you can also use these 2 methods to request any class once a method started running (and you did not inject something as parameters but you are asking for this information once the method is running).
One last thing, read Package Development as it can be really complex, I have not much expertise about creating a package but it will help you.
Upvotes: 0
Reputation: 53533
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();
Upvotes: 2