I would like to use the AlterPHP extension as well as the Wandi extension with EasyAdminBundle.
But we face some issue configuration both of them at the same time.
We used to have this config file when using only AlterPhp :
#routes/easy_admin.yaml
easy_admin_bundle:
resource: '#EasyAdminExtensionBundle/Controller/EasyAdminController.php'
prefix: /admin
type: annotation
And it was fine when we only used this bundle. However, now we want to use this bundle as well as the one quoted previously but it also needs to replace the easyadmin controller by the one from the new bundle.
So both extension wants to do the same thing and both extend the BaseAdminController from EasyAdmin.
What would be the best way to use both in the same project ?
I found a solution by making a custom controller that extends the AdminController from Wandi and copying the methods from the AdminController from Alterphp inside the custom controller. However, it seems like an odd solution to this problem.
I decided to contact both AlterPHP and Wandi on github and send a pull request on their extensions to use trait in their controller to make it easier to use multiple extensions.
So both of them answered to me :
Wandi reviewed my PR and merged it to master. It is now available in release 2.0.2.
AlterPHP reviewed my PR and merged it to master. It is now available in release 3.0.1
So with those changes it is way easier to use both extensions (and similar EasyAdminExtension) by using those new traits :
use Wandi\EasyAdminPlusBundle\Controller\AdminController as WandiController;
use EasyCorp\Bundle\EasyAdminBundle\Controller\EasyAdminController;
use Wandi\EasyAdminPlusBundle\Controller\AdminControllerTrait as WandiTrait;
use AlterPHP\EasyAdminExtensionBundle\Controller\AdminExtensionControllerTrait as AlterPHPTrait;
class CustomAdminController extends EasyAdminController
{
use AlterPHPTrait, WandiTrait;
//You may have to solve conflict between those traits
}
You may have multiple problems such as services not known by the controller or methods defined multiple time.
I just had to redefine getSubscribedServices in my controller to add those used by AlterPHP and Wandi, as well as resolving a conflict with the method isActionAllowed defined in both traits.
use AlterPHP\EasyAdminExtensionBundle\Security\AdminAuthorizationChecker;
use EasyCorp\Bundle\EasyAdminBundle\Controller\EasyAdminController;
use Wandi\EasyAdminPlusBundle\Controller\AdminControllerTrait as WandiTrait;
use AlterPHP\EasyAdminExtensionBundle\Controller\AdminExtensionControllerTrait as AlterPHPTrait;
use Wandi\EasyAdminPlusBundle\Exporter\Configuration\NormalizerConfigPass;
use Wandi\EasyAdminPlusBundle\Exporter\Configuration\PropertyConfigPass;
use Wandi\EasyAdminPlusBundle\Exporter\Configuration\TemplateConfigPass;
class CustomAdminController extends EasyAdminController
{
use AlterPHPTrait,WandiTrait { AlterPHPTrait::isActionAllowed insteadof WandiTrait; }
//It is important to set the subscribed services from the trait because they cannot use them otherwise.
public static function getSubscribedServices(): array
{
return \array_merge(parent::getSubscribedServices(), [
AdminAuthorizationChecker::class, //This one is for AlterPHP and those below for Wandi
'wandi.easy_admin_plus.exporter.configuration.normalizer_config_pass' => NormalizerConfigPass::class,
'wandi.easy_admin_plus.exporter.configuration.property_config_pass' => PropertyConfigPass::class,
'wandi.easy_admin_plus.exporter.configuration.template_config_pass' => TemplateConfigPass::class,
]);
}
}
I had to modify my services.yaml to be able to redefine getSubscribedServices for Wandi.
#services.yaml
services:
#...
Wandi\EasyAdminPlusBundle\Exporter\Configuration\NormalizerConfigPass: '#wandi.easy_admin_plus.exporter.configuration.normalizer_config_pass'
Wandi\EasyAdminPlusBundle\Exporter\Configuration\PropertyConfigPass: '#wandi.easy_admin_plus.exporter.configuration.property_config_pass'
Wandi\EasyAdminPlusBundle\Exporter\Configuration\TemplateConfigPass: '#wandi.easy_admin_plus.exporter.configuration.template_config_pass'
Related
First off, the issue/question arose with Laravel 5.6 and PHP 7.4.10. I know that 'magic strings' for controller calls have been deprecated as of recent Laravel versions, but I'm interested in the underlying infrastructure in this question.
PREREQUISITES
Suppose I have two folders: project and core.
project contains a freshly installed laravel project;
core contains some package-like structure of namespaces and classes (not actually a package in the laravel sense), which is being autoloaded by the project.
For a minimal working example, you can see the last commit of the repository I published about this. I'm going to post the main files' contents here as well, but you can actually reproduce and play around with the sample projects yourself.
Basically the concept is pretty standard, much like a package: I want to have some reusable logic inside of my core with some default implementations. In the project I may or may not overwrite some of the functions, but for the sake of the question's simplicity, my core only defines a Fruit module (just a folder with a namespace) containig three files:
fruit_routes.php
<?php
Route::get('/pear', 'FruitController#pear');
Route::get('/apple', 'FruitController#apple');
FruitController (This is where the question is focused)
<?php
namespace CORE\Fruit;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class FruitController extends Controller
{
public function pear()
{
//if we call the action '\CORE\Fruit\FruitController#apple' instead, it works :)
return redirect()->action('FruitController#apple', ['message' => "No pears available! :( "]);
}
public function apple(Request $request)
{
return $request->message . "There you have an apple!";
}
}
FruitServiceProvider
<?php
namespace CORE\Fruit;
use Illuminate\Support\ServiceProvider;
class FruitServiceProvider extends ServiceProvider
{
public function register()
{
/*
* Map routes from our Fruit module to the controller's namespace
*/
$this->app->router->namespace('CORE\Fruit')->group(__DIR__."/fruit_routes.php");
}
}
And then in the project I simply include the following two lines:
composer.json
{
//..
"autoload":
//...
"psr-4": {
"App\\": "app/",
"CORE\\" : "../core"
}
},
}
config/app.php in $providers array
/*
* Package Service Providers...
*/
CORE\Fruit\FruitServiceProvider::class,
Again, for the full sample structure and contents, refer to the MWE repo.
THE QUESTION
Although the namespace for the controller classes has been explicitly declared from the FruitServiceProvider's register() method for the fruit_routes.php file, Laravel still fails to recognize that same namespace when using rediret()->action() from within those same controllers.
In the given FruitController you can see:
return redirect()->action('FruitController#apple');
and this will fail with Action App\Http\Controllers\FruitController#apple not defined.
As you can see, Laravel still searches for the controller inside the current project's default controller namespace (in this case: App\Http\Controllers)
Fortunately, passing the fully qualified name of the controller solves the issue:
return redirect()->action([self::class, 'apple']);
//Or alternativelly, but really clumsy:
//return redirect()->action('\Core\Fruit\FruitController#apple'); //notice the slash at the beginning
I have the suspicion that I must somehow also inform the redirect() helper (and actually, the underlying Redirect facade) that I want it to reference the controllers inside of my CORE\Fruit namespace whenever called from within that namespace... Or what would be the right approach? Is that desired behavior and am I missing something? And again, as mentioned in the beginning, I know that 'magic strings' are not the best practice and the most beloved feature of Laravel, but I really want to understand how things work in this scenario.
I also found this issue on github with a realted topic, but no answer there as well. As for people who are about to comment 'just avoid using magic strings', I'm stuck with a project I inherited and rewriting all of the core's magic string controller references is really not an option I would love to embrace.
Any insight is appreaciated! Many many thanks in advance!
So subject is the question. Yes, I've searched this forum and googled too. All I've got - useless Symfony docs and casts, some general advises, cli-s and nothing case specific. Maybe yahoo or duckduck could help better?
Everyone is talking about bundles, about how it is important to create them, probably because under the hood Symfony is pushing users away from custom libraries, but no one is actually explains how to start using a bundle - how to start calling its methods.
No, my library is not a composer or whatever package. No, library methods do not return Response objects. No, I am not dealing with composer or recompilations or cli (I use Composercat). No, I will not put library to github or packagist to load it via composer or whatever because it is private library.
Sorry about emotional off-topic.
About the case: I've put my library into the folder
src/lib/MyLibrary.php
I suspect that library class is autoloaded, because if I do not extend Controller with it (if I declare class MyLibrary instead of class MyLibrary extends Controller) - Symfony spits "class name in use" error.
So question: in my controller how to call library method?
$this->get('MyLibrary') doesn't work.
echo print_r($this) doesn't show MyLibrary in this registry too.
Looks like library file is loaded but not registered and/or instantiated. If it is so, then where to point Symfony to register it?
So most of this question is really about how php manages classes. Not so much about Symfony. But that's okay.
To start with it would be best to move project/src/lib to just project/lib. Symfony has some scanning stuff going on in the src directory and you really don't want to have your library mixed up in it.
So:
# lib/MyLibrary.php
namespace Roman;
class MyLibrary
{
public function hello()
{
return 'hello';
}
}
Notice that I added a namespace (Roman) just to distinguish your code from Symfony's.
Now you need to tweak composer.json in order to allow php to autoload your classes:
"autoload": {
"psr-4": {
"App\\": "src/",
"Roman\\": "lib/"
}
},
After adding the Roman line, run "composer dump-autoload" to regenerate the autoload files.
After that, it's just a question of using regular php inside of your application:
# src/Controller/DefaultController.php
namespace App\Controller;
use Roman\MyLibrary;
use Symfony\Component\HttpFoundation\Response;
class DefaultController
{
public function index()
{
$myLibrary = new MyLibrary();
$hello = $myLibrary->hello();
return new Response($hello);
}
}
And that should get your started.
I'm developing a package which has controllers in it and I want this package to be compatible with (or useable by) both Laravel and Lumen projects. My problem is Laravel controllers extend Illuminate\Routing\Controller and Lumen controllers extend Laravel\Lumen\Routing\Controller. The controller inside my package can't extend them both.
The only solution I've come up with is to have the controllers inside the package extend App\Http\Controllers\Controller.
But I see some problems:
App\Http\Controllers\Controller should exist; which means it wouldn't work if the App namespace is named differently.
The package is now "aware" that it is being included in something.
Testability: I can't test the controller independently because of the reference to App\Http\Controllers\Controller.
Is there a better way of doing this?
Edit 1
I'm finding other classes which are affected in a similar way. For example, the namespace of the trait Authorizable is Illuminate\Foundation\Auth\Access in Laravel while it is Laravel\Lumen\Auth in Lumen. I am using a model which uses that trait. How do I make my model compatible with both Lumen and Laravel?
Well, you could simply have two different files and classes wrapped in if statements and check for the corresponding classes to extend. So:
LaravelClass.php:
if(class_exists(Illuminate\Routing\Controller:class)){
class LaravelClass extends Illuminate\Routing\Controller {
use YourCodeTrait;
// any more code that is not in your trait
}
}
LumenClass.php
if(class_exists(Laravel\Lumen\Routing\Controller:class)){
class LaravelClass extends Laravel\Lumen\Routing\Controller {
use YourCodeTrait;
// any more code that is not in your trait
}
}
Loading both files will only load one of the classes. In the code above I use a trait to load the contents of your controller, assuming the contents is the same, you could use the same trait and not have to repeat yourself.
trait YourCodeTrait{
// whatever would normally go in your controllers
}
I just want to know what is the best practise/way of doing this. I'm going to explain with an example to make it easier to understand.
Note: So far example below work fine if I have only one bundle in my application. Question is at the bottom of the post.
Thanks in advance
SingleBundle/Resources/config/services.yml
services:
form_errors:
class: Hello\SingleBundle\Services\FormErrors
FormErrors.php
namespace Hello\SingleBundle\Services;
use Symfony\Component\Form\FormInterface;
class FormErrors
{
public function getErrors(FormInterface $form)
{
.......
.......
return $errors;
}
}
QUESTION:
How do I avoid duplicating these two files if I have more than one bundles in my application? Where do I define service and the service class whcih will be accesible from all the bundles?
You have to define your service in the bundle which implements logic of this service. If you have bundle SingleBundle the best way to call the service is to use special prefix (single_bundle.form_errors in your case).
If you have two or more bundles it's not necessary to duplicate service definition and service class declaration because all services defined in the namespace of one bundle (which is properly loaded to the project) are accessible in the namespace of the other bundle (which is properly loaded to the project as well).
So, I think before create a service you just need to think where it should be defined. And take care about service name if you have any doubts about possible duplicates.
I'm trying to figure out how to add a method to a class in a Laravel package, so that all controllers and models that call that class can access the new method. How do I replace this class in the IoC?
This is the package in question, Angel CMS. The package is my creation, so I can modify it if we need to add aliases or anything to accomplish this.
Let's say I want to add a method to this class:
vendor/angel/core/src/models/PageModule.php
Okay, so I copy the class file to here:
app/models/PageModule.php
And then I modify the copied file, adding a namespace and the desired custom_function method:
<?php namespace MyModels;
use Eloquent;
class PageModule extends Eloquent {
protected $table = 'pages_modules';
public static function custom_function()
{
return 'It works!';
}
}
As you can see, I am using the MyModels namespace here.
Then, I run a composer dump-autoload.
Next, I open up my app/routes.php and register the binding and set up a test route:
App::bind('PageModule', function($app) {
return new \MyModels\PageModule;
});
Route::get('test-binding', function() {
return PageModule::custom_function();
});
But, when visiting the test route, I always receive the same error that the method is undefined.
What am I doing wrong here? Thank you in advance for any help.
To Clarify:
I am attempting to replace the class application-wide so that all other classes (controllers/models/etc.) that call PageModule will have access to the custom_function method. Thanks.
To be honest, I'm pretty new to all this IoC, dependency inversion/injection concept too. But I think I've gone through the same struggle before. What I would do, as much as my knowledge allows, is...
Add a constructor to src/controllers/admin/AdminPageController.php:
protected $pageModule;
public function __construct(PageModule $pageModule)
{
$this->pageModule = $pageModule;
}
Then where you did $module = new PageModule in the same file. You replace it with:
$module = $this->pageModule;
The two modifications above makes use of Laravel's IoC to allow injecting a different PageModule object into your controller, instead of strictly creating PageModule in your code.
Now at this point Laravel should know that when it constructs the AdminPageController, it should create a PageModule and inject into the controller for you.
Since your controller now expects a PageModule class, you can no longer do class PageModule extends Eloquent in your app anymore, because even though the name is the same, PHP does not think that it is! You'll need to extend it:
So let's rename your app/models/PageModule.php to app/models/CustomPageModule.php, and in the file change the class to:
class CustomPageModule extends \PageModule {
Up to this point, you also have a CustomPageModule class that is a child of your package's PageModule. All you need to do now is to let Laravel knows that if any controllers ask for PageModule, it should serve the controller with your MyModels\CustomPageModule instead.
So at the top of your app's routes.php file:
App::bind('PageModule', 'MyModels\CustomPageModule');
Your AdminPageController should now be using your CustomPageModule and can use whatever public methods that are in there!
I'm expecting to be editing this answer heavily since this will be quite a long discussion. My first try at answering above isn't the best code you can write, but I hope it takes the least amount of edit to the original code, and then we can work up from there.
Or fast track by reading up articles like http://culttt.com/2013/07/08/creating-flexible-controllers-in-laravel-4-using-repositories
You probably have a alias for the PageModule facade, you should override this alias using your class \MyModels\PageModule in your app/config/app.php file.
Be careful, it seems like you are overwriting the PageModule class instead of extending it. You should probably extend the parent class instead of Eloquent.