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.
Related
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?
I am using the Model-View-Controller framework to structure my files for readable, reusable, and refactorable purposes.
My goal is to invoke methods from two seperate classes while working with one class. In practice; I want to access the methods for example in file Model.js and View.js from Controller.js.
Previously in ES5 I've had one file called app.js that used the IIFE approach:
var View = (function(){
dump(){
console.log('Hello World');
}
});
var Model = (function(){
// Code goes here
});
var Controller = (function(viewCtrl, viewCtrl){
viewCtrl.dump(); // Invoke method from View
})(View, Model);
As shown above, I would like to do something similar in ES6 too.
import View from './View';
import Model from './Model';
class Controller {
dump(){
return viewCtrl.dump();
}
init(){
console.log('Application has started');
// Make a new object of the class { View, Model }
let view = new View();
let model = new Model();
}
}
export default Controller;
In my main.js:
import Controller from './Controller';
// Make a new object of the class Controller
let controller = new Controller();
// Instantiate App
controller.init();
console.log(
controller.dump()
);
But doing so in ES6 I get error: ReferenceError: viewCtrl is not defined in main.js.
I was thinking perhaps pass View.js and Model.js as arguments in a constructor inside the Controller.js in order to declare them. But I guess it might be a better solution that looks cleaner.
So what I am basically looking for is to apply the MVC pattern using ES6, any recommendations?
Passing the instances of View and Model to the constructor of Controller is a clean solution since this would fullfill the dependency-injection-pattern.
This way you get the ability to change the instances from outside which makes the Controller testable.
In a situation where View and Model where singletons you could export them as instances instead of classes like
View.js:
export default new View();
Controller.js:
import view from "./View";
...
view.dump()
Using this way you could spare the work of passing a model and a view to a Controller manually but that would also mean that you loose the ability to test Controller or to change parts of it, like the View. Since this is one of the best advantages of the MVC-pattern, I would not recommend to import singletons. Instead I would recommend to inject View and Model using a constructor.
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!
I would like to have a method available to all Views in my app.
I would like to be able to make calls like this:
<span>${ getDynamicText() }</span>
The most obvious ways (to me) to implement this are:
Call the method in the controller and and pass it to the View.
Make the method static on some Util class and call it from the code ${ UtilClass.getDynamicText() }
Using meta programming to somehow make the method available to all Views.
The benefit of #3 is that the change would only have to be made in one place. #1 would have to be made in each controller action; and #2 would need an import on every View page which wants to use the method.
So is there a way to add a method to be available to all views in my app?
I have to admit I don't know in a lot of detail how .gsp files are processed behind-the-scenes so maybe they don't have a corresponding class and therefore can't be manipulated in this way. Links to good articles/docs will get extra good karma.
GSPs are compiled into classes that extend org.codehaus.groovy.grails.web.pages.GroovyPage, so you can add methods to that metaclass and they'll be available to all GSPs. The best place to do this is in BootStrap.groovy (or in a plugin's doWithDynamicMethods closure):
import org.codehaus.groovy.grails.web.pages.GroovyPage
class BootStrap {
def init = { servletContext ->
GroovyPage.metaClass.getDynamicText = { ... }
}
}
The recommended way to reuse functionality across GSPs is to define it as a tag, e.g.
class MyTagLib {
static namespace = 'my'
def dynamicText = {attrs ->
out << 'not very dynamic'
}
}
You can then call this tag in a GSP using:
<my:dynamicText/>
4th way: make a class/service that have method '.getDynamicText' and put it's intance into request at before filter ( request.setAttribute('x', myDynamicTextGeneratorObject) )
Now you can use x.dynamicText in any GSP
This is how I would do it:
Add a new class to your controllers folder containing your method
Do a grails install-templates
Navigate to the templates: \src\templatesscaffolding
Add the extends part to the controller template: class ${className}Controller extends NewController
re-generate your controllers
You can now use the method in every class and gsp.
I'm having Codeigniter object scope confusion.
Say I load a model in a controller:
$this->load->model('A');
$this->A->loadUser(123); // loads user with ID 123
// output of $this->A now shows user 123
$this->load->model('B');
$this->B->examineUser ();
// output of $this->A now shows user 345
class B extends Model
{
public function examineUser ()
{
$this->load->model('A');
$this->A->loadUser(345); // loads user with ID 345
}
}
I would have thought that $this->A would be different from $this->B->A but they are not. What is the best solution to this issue? It appears the ->load->model('A') in the examineUser () method does nothing because it was loaded in the controller. Then the call to loadUser () inside that method overwrites the stored properties of $this->A. This seems like a bugfest waiting to happen. If I needed global models, I would have use static classes. What I wanted was something scoped pretty much locally to the model object I was in.
Is there a way I can accomplish this but not go way outside of CI's normal way of operating?
Followup/related:
Where do most people put there "->load->model" calls? All at the beginning of a controller action? I figured it would be easier -- though perhaps not excellent programming from a dependency injection perspective -- to load them in the model itself (construct or each method).
Whenever you use the Loader Class ($this->load->), it will load the object into the main CI object. The CI object is the one you keep referring to as $this->. What you've done is load model A twice into the CI object.
Essentially, all object loaded using the Loader class goes into a single global scope. If you need two of the same type, give them different names, as per $this->load->model('A','C'). I don't know of any way around it unless you revert to using bog-standard PHP.
In my team's code, we generally load the models in the controller's constructor, then load the data to send to the view in the function, often _remap().
This is not how the loader works sadly. CodeIgniter implements a singleton pattern, which will check to see if the class is included, instantiated and set to $this->A then will be ignored if loaded again. Even if you are inside a model, $this->A will be referenced to the super-instance via the __get() in class Model. Alis it, or just do:
class B extends Model
{
public function examineUser ()
{
$user = new A;
$user->loadUser(345); // loads user with ID 345
}
}
Here's what I've decided to do, please comment if you have advice:
I've extended the CI Loader class:
<?php
class SSR_Loader extends CI_Loader
{
function __construct()
{
parent::__construct ();
}
/**
* Model Retriever
*
* Written by handerson#executiveboard.com to create and return a model instead of putting it into global $this
*
* Based on original 2.0.2 CI_Loader::model ()
*
*/
function get_model($model)
{
if (empty ($model))
{
return;
}
$name = basename ($model);
if (!in_array($name, $this->_ci_models, TRUE))
{
$this->model ($model);
}
$name = ucfirst($name);
return new $name ();
}
}
Do any CI guru's see a problem with that before I invest time in changing my code a bit to accept the return obj, ala:
// in a controller:
public function test ($user_id=null)
{
$this->_logged_in_user = $this->load->get_model ('/db/users');
$this->_viewed_user = $this->load->get_model ('/db/users');
$this->_logged_in_user->load($this->session->userdata ('user.id'));
$this->_viewed_user->load($user_id);
}
I could also do private $_logged_in_user to make it available in the controller but positively force it to be limited to just the current controller and not spill anywhere else, or I could just do $_logged_in_user = $this->load->get_model ('/db/users'); and limit it to just the current method, which is probably what I'll do more often.
This seems like a pretty straightforward way to "fix" this issue (I say "fix" b/c it's not really a bug, just a way of doing things that I think is a bad idea). Anyone see any flaws?