How to resolve a contextually bound class from Laravel IOC container - laravel

I have a command class with a dependency type-hinted in the constructor:
class RunAnalyticsCommand extends Command
{
public function __construct(Analytics $analytics)
{
//
}
}
The constructor for the Analytics class looks like this:
class Analytics
{
public function __construct(Odoo $odoo, ChannelInterface $channel)
{
//
}
}
In a service provider, I've instructed the application what to instantiate for the Odoo class. If I create an instance of Analytics like this, it works fine. It gets the Odoo instance from the container and uses the channel that I pass in.
$analytics = app(Analytics::class, ['channel' => new ChannelA]);
Now, I'm trying to use contextual binding to accomplish all of this "behind the scenes". I write the following in my service provider's register method:
$this->app->when(RunAnalyticsCommand::class)
->needs(Analytics::class)
->give(function () {
return app(Analytics::class, ['channel' => new ChannelA]);
});
However, now when I run the RunAnalyticsCommand, I get an error that Maximum function nesting level of '256' reached, aborting!
I'm assuming this happens because the give callback is trying to resolve the same Analytics class, and the container treats that call as if it was also coming from the RunAnalyticsCommand class, so it just keeps trying to resolve the same thing over and over.
Is this the expected behavior or a bug with contextual binding? Shouldn't the call to resolve the class from within the give callback not behave as if it were originating from the RunAnalyticsCommand? Is there some other way to tell the container to resolve without using the contextual binding?

Related

Laravel-Class Method with Reflected Parameters

I usually use parameters like this:
public function test($parameter)
{
echo 'Parameter value: ' . $parameter;
}
While looking at laravel service container I see this code.
public function __construct(UserRepository $users)
{
$this->users = $users;
}
According to the documentation it uses reflection.But i dont understand.
I dont know how the parameter UserRepository $users works. Is that an alias or something?
This is called type-hinting and is used to inject dependencies in a constructor or to validate the right type of argument is passed to a function. The injection simply means that if the class is called with the make method, Laravel will automatically provide an instance of the class required by your constructor.
For example if you have a function public function something(string $something) it would throw an error if any other type than a String is passed to this function, making sure the right data is used.
From the laravel documentation:
Alternatively, and importantly, you may "type-hint" the dependency in the constructor of a class that is resolved by the container, including controllers, event listeners, queue jobs, middleware, and more. In practice, this is how most of your objects should be resolved by the container.
For example, you may type-hint a repository defined by your application in a controller's constructor. The repository will automatically be resolved and injected into the class:
Laravel has a great service container and it makes all dependency injections, so you don't need to pass a class a parameter, laravel do it for you.
without container you have to pass this parameter
class A {
public $foo;
public function __construct (Foo $foo){
$this->foo
}
$classA = new A((new Foo))
When laravel encounter with these classes, it resolves them.
Also you can define manually these classes using singleton() or bind() methods
$this->app->singleton('FooBar', function($app)
{
return new FooBar($app['SomethingElse']);
});
Or you may use interfaces. You can bind implemented class for to the interface and laravel when encounter with that interfance, it will resolve as you wish
$this->app->bind('App\ICacheManager', 'App\RedisManager');
public $redis;
public function __contruct(ICacheManager $redis){
$this->redis = $redis;
}
for more further check out laravel service container

Saving an object into the session or cookie

I'm using Instagram API library to connect user to Instagram profile and then do smth with it. So, as Instagram API wiki says:
Once you have initialized the InstagramAPI class, you must login to an account.
$ig = new \InstagramAPI\Instagram();
$ig->login($username, $password); // Will resume if a previous session exists.
I have initialized InstagramAPI class and then I called $ig->login('username', 'password');. But I have to call it in every function where I need to work with Instagram.
So how could I save this object $ig for using it in the future in other controllers without calling login() any more? Can I save $ig object into the session or cookie file?
P.S. I think saving into the session is not safe way to solve the issue.
UPD: I was trying to save $ig object into the session, however the size is large and session become stop working as well.
Regarding the register method you asked in the comments section, all you need to create a new service provider class in your app\providers directory and declare the register method in there for example:
namespace App\Providers;
use InstagramAPI\Instagram;
use Illuminate\Support\ServiceProvider;
class InstagramServiceProvider extends ServiceProvider
{
public function register()
{
// Use singleton because, always you need the same instance
$this->app->singleton(Instagram::class, function ($app) {
return new Instagram();
});
}
}
Then, add your newly created InstagramServiceProvider class in providers array inside the config/app.php file for example:
'providers' => [
// Other ...
App\Providers\InstagramServiceProvider::class,
]
Now on, in any controller class, whenever you need the Instagram instance, all you need to call the App::make('InstagramAPI\Instagram') or simply call the global function app('InstagramAPI\Instagram') or even you can typehint the class in any method/constructor etc. Some examples:
$ig = App::make('InstagramAPI\Instagram');
$ig = App::make(Instagram::class); // if has use statement at the top fo the class
$ig = app('...');
In a class method as a dependency:
public function someMethod(Instagram $ig)
{
// You can use $ig here
}
Hope this helps but read the documentation properly, there will get everything documented.

Load objects with parameters from Laravel IoC container

I need a way to load objects via IoC provider depending on a request parameter. Right now I'm loading my objects directly with App::make(xy, [$urlParamter] which I want to refactor, so that I can use dependency injection the way it is supposed to. To describe my current architecture I need to show you quiet some information and in the end you find my concrete questions I have about it.
I'm building up a general CMS framework which provides an import architecture that is extendable with a custom import implementation.
Now I'm struggling with properly loading the concrete classes via IoC container, because they always depend on the selected import.
To dig into my problem, here is my entry point in routes.php
Route::get('/import', ['as' => 'overview', 'uses' => '\CMSFramework\Http\Controllers\Import\ImportController#index']);
This generates a view where the user selects a concrete import to be triggered. After selecting a concrete import, the user should get individual views to prepare the appropriate import (i.e. Upload a CSV file, select an area to import, etc.)
In my concept an import implementation consist of:
A controller class, to implement specific (peraration-) tasks like uploading a CSV file. It inherits from a base controller of the cms framework
An import "business" or "service" class, that implements how the data is getting imported (and may further delegate to queued jobs etc.)
The CMS framework part consists of:
A base controller class for all common/shared import tasks like (start the prepared import, clean all working data, etc.)
A base service class ImportBase where all implementations inherit from. It provides an interface to receive a progress for any import and implements shared operations like cleaning up working data, etc.)
An ImportStatus class which is part of the ImportBase-Class via $ImportBase->status() to handle all runtime status informations (like "is the job still running, what is the progress). This class also provides a containter for a so called "payload" that allows any conrete import implementation to push and fetch custom status informations (ie. any sub-process has been finished)
So back to my IoC architecture. After the user selected a concrete import, the following route delegates the action to the custom import implementation's controller. If it's a framework supported standard-action like via URL /import/<importkey>/clean, the inherited BaseController of the cms framework takes over and handles the request
Route::get('/import/{key}/{method}', ['uses' => function($key, $method) {
return App::make('\\MadeleinePim\\Http\\Controllers\\Import\\'.ucfirst(camel_case($key)).'Controller')->$method($key);
}]);
I know that this direct binding via a naming convention can be improved (maybe via a custom configuration file), but for now this works for me.
Now I need to show an example of how I tried to implement a concrete import target in my controller via /import/<importkey>/seedCsvDataToDatabase:
public function seedCsvDataToDatabase($key)
{
// The IoC binding is shown in next code snippet. I did not found a good way to use method injection because
// of the route-specific parameters that control the responsible import implementation
$import = \App::make(Import::class, [$key]);
// Now trigger the import service operation of that concrete import implementation (probably bad design here)
$import->seed();
// Now, that this preparation task is done, I use the ImportStatus object which is part of the Import to store
// status informations. With this I can then decided in which step the user is (Think of it like a wizard to
// prepare any import)
$import->status()
->set(ConcreteImport::STATUS_SEEDED, true)
->set(ConcreteImport::STATUS_SEEDED_DURATION_SECONDS, (microtime(true) - $time_start) / 60);
// Back to controller method that determines in which status the import is to delegate/redirect to different
// views.
return redirect('/import/<importkey>');
}
My IoC binding for the Import class:
$this->app->singleton(Import::class, function ($app, array $parameters) {
$importKey = head($parameters);
// There is a config file that provides the class names of the concrete import implementations
$importClassName = config()->get('import.' . $importKey);
if (!$importClassName) {
throw new ImportNotFoundException($importKey, "Import with key '{$importKey}' is not setup properly'");
}
$importReflectionClass = new \ReflectionClass($importClassName);
return $importReflectionClass->newInstance($importKey);
});
And finally, the lazy loading of the import status, which is encapsulated in the ImportStatus object looks like this
public function status()
{
if (!$this->status) {
$this->status = \App::make(ImportStatus::class, [$this->key()]);
}
return $this->status;
}
I hope that demonstrates the way I try to resolve my import objects from the IoC container.
My learning so far is, that this is not the right way to inject my objects.
Is the assumption right, that I should not pass the $importKey at runtime to the App::make() and rather should try to make this independ?
My failed attempt on this was to make the IoC binding smarter and let it access the Request to properly inject my concrete import object with the required $importKey, like (pseudo code!):
$this->app->bind(ImportStatus::class, function(Container $app) {
// Did not find a good way to access the {key}-part of my route /import/{key}/{method}
$key = $app->make(Request::class)->get('key'); // Does not work like this
return new \Scoop\Import\ImportStatus($key);
});
Does this approach can work like this?
Can I somehow pass through the $importKey from my route to the ServiceProvider (or better pull it from there?)
Is there a better solution to initialize my concrete import implementations?
----------
UPDATE 1
For my lattest idea to access the Route in my IoC Binding, I got this way working:
$this->app->singleton(Import::class, function (Container $app) {
$importKey = \Route::current()->getParameter('key');
$importClassName = config()->get('import.' . $importKey);
$importReflectionClass = new \ReflectionClass($importClassName);
return $importReflectionClass->newInstance($importKey);
});
Nevertheless the idea of #Sandyandi N. dela Cruz to use a router binding prevents the direct coupling between the Binding and the Request which still doesn't feel right. Using router-binding to couple a request parameter to an implementation, sounds more appropriate.
I think you've dwelt to much on the IoC container there. Why not implement the Factory pattern and do a route binding instead of creating multiple controllers to handle different Imports? Crude example as follows:
Create a route binder - edit your app/Provider/RouteServiceProvider.php's boot() method
public function boot(Router $router)
{
parent::boot($router);
// Add the statement below.
$router->bind('import', 'App\RouteBindings#import');
}
Create the App\RouteBindings class as app/RouteBindings.php
Create an import() method with the following:
public function import($importKey, $route)
{
switch ($importKey) {
case 'import_abc':
return new ImportAbc;
break; // break; for good measure. ;)
case 'import_xyz':
return new ImportXyz;
break;
// and so on... you can add a `default` handler to throw an ImportNotFoundExeption.
}
}
Create a route for resolving an Import class.
Route::get('import/{import}/{method}', 'ImportController#handleImport');
Here, {import} will return the proper Import concrete class based on your URL.
In your ImportController's handleImport() you can do the following:
public function handleImport(Import $import, $method)
{
// $import is already a concrete class resolved in the route binding.
$import->$method();
}
So when you hit: http://example.com/import/import_abc/seed, the route binding will return a concrete class of ImportAbc and store it in $import on your handleImport() method, then your handleImport() method will execute: $import->seed();. Tip: you should probably move other controller logic such as $import->status()->set() into the Import class. Keep your controllers thin.
Just make sure your Import classes have the same signature.
It's kinda like Laravel's Route Model Binding except you create the logic for the bindings.
Again, this is just a crude example but I hope it helps.

Zend framework 2 : Add different authentication adapter for two different modules

I have two different modules. Now I need to add different authentication mechanism for both modules.
So I added event code first module's Module.php's onBootstrap method
$listener = $serviceManager->get('First\Service\AuthListener');
$listener->setAdapter($serviceManager->get('First\Service\BasicAuthAdapter'));
$eventManager->attach(MvcEvent::EVENT_ROUTE, $listener, 0);
and in second module's Module.php's onBootstrap method
$listener = $serviceManager->get('Second\Service\AuthListener');
$listener->setAdapter($serviceManager->get('Second\Service\AdvAuthAdapter'));
$eventManager->attach(MvcEvent::EVENT_ROUTE, $listener, 0);
Now if I disable one of modules, functionality working fine and request properly authenticated. While enabling both module do some kind of overlapping So even required module properly authenticated, But other module event code also got executed and system give not authenticated error.
I am thinking this due to event handler code in both module.php is executed without take care of requested module url.
I can verify with requested route pattern before authentication, But that is look like a hack instead of good solution.
If better solution exists for handling this issue ?
UPDATE :
My AuthListener Code :
namespace First\Service;
use Zend\Authentication\Adapter\AdapterInterface;
use Zend\Mvc\MvcEvent;
class AuthListener
{
protected $adapter;
public function setAdapter(AdapterInterface $adapter)
{
$this->adapter = $adapter;
}
public function __invoke(MvcEvent $event)
{
$result = $this->adapter->authenticate();
if (!$result->isValid()) {
$response = $event->getResponse();
// Set some response content
$response->setStatusCode(401);
$routeMatch = $event->getRouteMatch();
$routeMatch->setParam('controller', 'First\Controller\Error');
$routeMatch->setParam('action', 'Auth');
}
}
}
There is a good way to make module specific bootstrap - to use SharedManager:
$e->getApplication()->getEventManager()->getSharedManager()
->attach(__NAMESPACE__, 'dispatch', function(MvcEvent $e) {
// This code will be executed for all controllers in current __NAMESPACE__
}, 100);
Here is a good article to understand difference between EventManager and SharedEventManager
There is no additional info about listeners in the question, but I try to guess:
If you use as listener some callable class - it's ok, just replace function() { } by your $listener.
If you use as listener some class, that implements
ListenerAggregateInterface, you should convert listeners to
SharedListenerAggregateInterface and use method attachAggregate
instead of attach
I hope it helps!

How to get a reference to the service manager inside the Module's init method (ZF2)?

I need that some code be executed before any MvcEvent::EVENT_BOOTSTRAP listener get execute. Evidently Module::onBootstrap is no an option. I end with the following code:
class Module
{
function init(\Zend\ModuleManager\ModuleManager $moduleManager)
{
$moduleManager->getEventManager()->attach(
MvcEvent::EVENT_BOOTSTRAP, array(ClassX, 'StaticMethodOfClassX'), 20000);
}
}
I don't want have hard code the array(ClassX, 'StaticMethodOfClassX') reference but get it from the service manager. My problem is that I don't know how to get an service manager reference inside the module's init method. Any help? or this is impossible in ZF2 right now? Whatever variant to this schema or opinion will be appreciate too ;)
EDIT:
I will clarify "Evidently Module::onBootstrap is no an option", cos may be is not so trivial ;)
Modules Module::onBootstrap methods are executed when the event MvcEvent::EVENT_BOOTSTRAP is triggered, but the attachment of each module's Module::onBootstrap method to that event depend of the order in which modules were loaded. Due to, the order in which a specific Module::onBootstrap method will be executed depend on what other modules exist and how other modules affect the order in which that specific module will be loaded. Beside, whatever listener attached to the MvcEvent::EVENT_BOOTSTRAP event with priority greater than 1 will be execute before any module Module::onBootstrap method, example the ViewManager::onBootstrap listener. So, to achieve what I want
I need that some code be executed before any
MvcEvent::EVENT_BOOTSTRAP listener get execute
modules obBootstrap methods are not an option.
This is a very old post but since no answer has been accepted and I recently needed to achieve the same thing, I thought I'd share my solution.
The reason I needed to access the ServiceManager before the Bootstrap event is triggered, was so I could manipulate the merged configuration with values retrieved from the database.
Problem:
The example found in the Zend documentation shows how to manipulate the merged configuration, but at that particular time the Service manager is empty, making it impossible to retrieve things like database adapters etc.
Solution:
In your module class, implement the interface InitProviderInterface and add the appropriate method.
public function init(ModuleManagerInterface $moduleManager)
{
$eventManager = $moduleManager->getEventManager();
$eventManager->attach(ModuleEvent::EVENT_LOAD_MODULES_POST, [$this, 'onLoadModulesPost']);
}
The EVENT_LOAD_MODULES_POST event will get invoked after the EVENT_MERGE_CONFIG event but before the EVENT_BOOTSTRAP event is triggered. Also at this particular time the ServiceManager will contain all the factories, invokable classes you're wanting to access.
Your callback method may look something like.
public function onLoadModulesPost(ModuleEvent $event)
{
/* #var $serviceManager \Zend\ServiceManager\ServiceManager */
$serviceManager = $event->getParam('ServiceManager');
$configListener = $event->getConfigListener();
$configuration = $configListener->getMergedConfig(false);
$someService = $serviceManager->get('Your/Custom/Service');
$information = $someService->fetchSomeInformation();
$configuration = array_merge($configuration, $information);
$configListener->setMergedConfig($configuration);
$event->setConfigListener($configListener);
$serviceManager->setAllowOverride(true);
$serviceManager->setService('Config', $configuration);
$serviceManager->setAllowOverride(false);
}
You can get it off the MvcEvent
$locator = $event->getTarget()->getServiceLocator()->get('YourObject')
If you don't have access to the event, you can set the event as a property on the Module class on bootstrap, and then use it in your init method whenever.
public function onBootstrap($event) {
$this->setMvcEvent($event);
}
function init(\Zend\ModuleManager\ModuleManager $moduleManager)
{
$locator = $this->mvc_event->getTarget()->getServiceLocator()->get('YourClass');
$moduleManager->getEventManager()->attach(
MvcEvent::EVENT_BOOTSTRAP, array(ClassX, 'StaticMethodOfClassX'), 20000);
}
Are you using ZfcBase in your application? The AbstractModule has a boostrap method (not onBootstrap) which is executed by this event handler in the init method
$sharedManager->attach('Zend\Mvc\Application', 'bootstrap', function($e) use ($instance, $moduleManager) {
$app = $e->getParam('application');
...
$instance->bootstrap($moduleManager, $app);
});
Of course you can use this approach without ZfcBase.
Then you can implement Zend\ServiceManager\ServiceLocatorAwareInterface:
public function bootstrap(\Zend\ModuleManager\ModuleManager $moduleManager, \Zend\Mvc\ApplicationInterface $app){
$this->setServiceLocator($app->getServiceManager());
parent::bootstrap($moduleManager, $app);
}
public function setServiceLocator(\Zend\ServiceManager\ServiceLocatorInterface $serviceLocator){
$this->_serviceLocator = $serviceLocator;
return $this;
}
public function getServiceLocator(){
return $this->_serviceLocator;
}
Maybe a bit later but hope it will help somebody else. At the init point there is no much services at the Service Manager but you can access it:
public function init(ModuleManager $moduleManager)
{
$sm = $moduleManager->getEvent()->getParam('ServiceManager');
$applicationConfig = $sm->get('applicationconfig');
var_dump($applicationConfig['modules']);
}
In this case we are retrieving the module names.
This is better idea.
class module
public function onBootstrap(MvcEvent $e)
{
$sm = $app->getServiceManager();
$config = $sm->get('config');
and this is all.

Resources