I have a long list of dependency injections to display a page with an article, navigation, etc. And currently I put them in a file called index.php to glue them together.
index.php,
use MyCustomVerndorName\Constant\Mapper\ConstantsMapper;
use MyCustomVerndorName\Slug\Mapper\SlugMapper;
.... (more)
$ConstantService = new ConstantService();
$ConstantController = new ConstantController();
$ArticleService = new ArticleService();
$ArticleController = new ArticleController();
// Prepare Nav model.
$NavModel = new NavModel();
$NavMapper = new NavMapper($PdoAdapter);
$NavService->setMapper($NavMapper)->setModel($NavModel);
// Prepare Article model.
$ArticleModel = new ArticleModel();
$ArticleMapper = new ArticleMapper($PdoAdapter);
// Prepare components.
$ArticleContentComponent = new ArticleContentComponent($PdoAdapter);
... (more)
// Inject components.
$ArticleMapper->addComponent($ArticleContentComponent);
... (more)
$NavChildrenComponent = new NavChildrenComponent($PdoAdapter);
... (more)
// Inject components.
$NavMapper->addComponent($NavChildrenComponent);
$NavMapper->addComponent($NavLanguageComponent);
// Controll the slug.
$SlugController->setService($SlugService)->fetchRow([
"url" => $url
]);
// Control the nav.
$NavController->setService($NavService)->fetchRows();
// Controll the article.
$ArticleService->setMapper($ArticleMapper)->setModel($ArticleModel);
$ArticleController->setService($ArticleService)->fetchRow([
"url" => $url
]);
// Prepare template.
$PageTemplate = new PageTemplate();
// Prepare view.
$ArticleView = new ArticleView($PageTemplate, $ArticleModel);
$ArticleView->setSlug($SlugModel);
$ArticleView->setNav($NavModel);
// Render the view.
echo $ArticleView->render('index.php');
in my router.php (I'm using AltoRouter),
use AltoRouter as Router;
$Router = new Router();.
$Router->map('GET', '/', '/article/container');
... (and other routes)
if($match)
{
$target = $match['target'];
$url = isset($match['params']['url']) ? $match['params']['url'] : DEFAULT_HOMEPAGE;
$language = isset($match['params']['language']) ? $match['params']['language'] : null;
include __DIR__ . $target . '.php';
}
I'm thinking to make the list of dependency injections in index.php into a container class so I can call this class whenever it is needed,
For instace in the router.php,
$Router->map('GET', '/', 'MyCustomVerndorName\Article\Container\ArticleContainer');
....
new $target($PdoAdapter, $url, $language);
And this container class,
namespace MyCustomVerndorName\Article\Container;
// Mapper.
use MyCustomVerndorName\Constant\Mapper\ConstantsMapper;
...
class ArticleContainer
{
/*
* Construct dependency.
*/
public function __construct(\MyCustomVerndorName\Adapter\PdoAdapter $PdoAdapter, $url, $language)
{
$ConstantService = new ConstantService();
$ConstantController = new ConstantController();
// Slug.
$SlugService = new SlugService();
$SlugController = new SlugController();
// Nav.
$NavService = new NavService();
$NavController = new NavController();
// Article.
$ArticleService = new ArticleService();
$ArticleController = new ArticleController();
// Prepare Article model.
$ArticleModel = new ArticleModel();
$ArticleMapper = new ArticleMapper($PdoAdapter);
// Prepare components.
$ArticleContentComponent = new ArticleContentComponent($PdoAdapter);
...
// Inject components.
$ArticleMapper->addComponent($ArticleContentComponent);
...
// Control the nav.
$NavController->setService($NavService)->fetchRows();
// Controll the article.
$ArticleService->setMapper($ArticleMapper)->setModel($ArticleModel);
$ArticleController->setService($ArticleService)->fetchRow([
"url" => $url
]);
// Prepare template.
$PageTemplate = new PageTemplate();
// Prepare view.
$ArticleView = new ArticleView($PageTemplate, $ArticleModel);
$ArticleView->setSlug($SlugModel);
$ArticleView->setNav($NavModel);
// Render the view.
echo $ArticleView->render('index.php');
}
}
Basically, I put all the dependency list into __construct,
public function __construct(\MyCustomVerndorName\Adapter\PdoAdapter $PdoAdapter, $url, $language)
{
...
}
So, is this the correct way of doing a DI Container without relying on the well known containers out there for PHP (such as Pimple, Symfony\DependencyInjection, etc)?
Traditionally, a DI container returns services upon demand. What you are calling ArticleContainer is really just a function then renders a view. It does a lot of DI injecting but it's not a container.
Let's assume you had a real container all properly initialized. Your code would look like:
$container = new Container();
// Some initialization
// Now use it
$viewArticle = $container->get('view_article');
echo $viewArticle->render('index.php');
Hopefully you can see the difference. Once the initialization is complete your application just pulls out the view it needs and renders it. No need to worry about pdo or url etc.
So how do we get our article view service into the container?
$container = new Container();
// Some initialization
$container->set('page_template',function($container)
{
return new PageTemplate();
}
$container->set('article_view',function($container)
{
$articleView = new ArticleView(
$container->get('page_template',
$container->get('article_model')
);
$articleView->setSlug($container->get('slug_model');
$articleView->setNav ($container->get('nav_model');
return $articleView;
};
// Now use it
$viewArticle = $container->get('article_view');
echo $viewArticle->render('index.php');
So we defined two service functions for our container. When we do $container->get('article_view');, the container calls the function with the container itself as an argument. The function creates the desired class by pulling dependencies out of the container and then returns the new object.
One assumes that you will probably have more page views in your application and that most of them will need a page_template. The same page_template service we defined can be used by your other views as well. So we start to get some reuse out of the container.
You basically just continue to add services to the container.
You can use Pimple for this but it's also instructive to make your own container. Really not much to it.
Related
I want to create a set of dependencies instead of injecting them everywhere. Would the factory pattern support this idea? Or is there another pattern for dealing with it?
For example:
class PassportCheckFactory {
protected $clientInstance;
protected $responseInstance;
public function buildDependancies() : bool
{
$this->clientInstance = new PassportCheckSoapClient;
$this->responseInstance = new PassportCheckResponse;
return true;
}
public function getResponseInstance()
{
return $this->responseInstance;
}
public function getClientInstance()
{
return $this->clientInstance;
}
}
This creates and holds the classes we would use, so we wouldn't need to inject them.
For example, we can do this
$request = new WhateverRequestClass;
$factory = (new PassportCheckFactory)->buildDependancies();
$service = new PassportCheckService($request, $factory);
$response = $service->execute();
instead of:
$request = new WhateverRequestClass;
$service = new PassportCheckService($request, new PassportCheckSoapClient, new PassportCheckResponse);
$response = $service->execute();
Your approach makes sense, if you want to support multiple CheckServices.
If PassportCheckService is the only one, the factory / service locator / specialised container from your example is just adding overhead.
$request = new WhateverRequestClass;
$service = new PassportCheckService($request, new PassportCheckSoapClient, new PassportCheckResponse);
$response = $service->execute();
is actually the best solution for a stand-alone service in terms of readability, maintainability and testabilty.
Multiple CheckServices
However, if you want to support multiple services, extracting the composition of the service into its own class brings benefits.
class CheckServiceFactory
{
public static function getService(Request $request, string $serviceType): CheckService
{
$type = ucfirst(strtolower($serviceType));
$serviceClass = $type . "CheckService";
$clientClass = $type . "CheckSoapClient";
$responseClass = $type . "CheckResponse";
return new $serviceClass($request, new $clientClass, new $responseClass);
}
}
Of course, the generation of the classnames depends on your naming scheme.
Calling a specific service would look like this:
$request = new WhateverRequestClass;
$service = CheckServiceFactory::getService($request, 'Passport');
$response = $service->execute();
Proposed Refactoring
Beside of the things above, I'd recommend to refactor the service class itself, so the request gets out of the constructor. That changes the usage
to:
$request = new WhateverRequestClass;
$service = CheckServiceFactory::getService('Passport');
$response = $service->handle($request);
or in case of a single service:
$request = new WhateverRequestClass;
$service = new PassportCheckService(new PassportCheckSoapClient, new PassportCheckResponse);
$response = $service->handle($request);
which actually looks much more straight forward.
Is it possible get the value of a route placeholder within a Slim container? I know I can access the placeholder by adding a third parameter to the request but I'd like to have it injected so I'm not assigning it on each request.
I've tried $app->getContainer('router') but I can't seem to find a method to actually pull the placeholder value.
Example:
$app = new Slim\App;
$c = $app->getContainer();
$c['Controller'] = function() {
$userId = // how do I get the route placeholder userId?
return new Controller($userId);
};
$app->get('/user/{userId}','Controller:getUserId');
class Controller {
public function __construct($userId) {
$this->userId = $userId;
}
public function getUserId($request,$response) {
return $response->withJson($this->userId);
}
}
Without some 'hacky' things this will not work because we have no access on the request object build by slim, while the controller get constructed. So I recommend you to just use the 3rd parameter and get your userid from there.
The 'hacky' thing would be todo the same, what slim does when you execute $app->run(), but if you really want todo this, here you'll go:
$c['Controller'] = function($c) {
$routeInfo = $c['router']->dispatch($c['request']);
$args = $routeInfo[2];
$userId = $args['userId'];
return new Controller($userId);
};
Note: slim3 also urldecoded this values so may do this as well urldecode($args['userId']) Source
create a container wrapper and a maincontroller then extend all your controller from your maincontroller, then you have access to the container.
here is how i solved this problem:
https://gist.github.com/boscho87/d5834ac1ba42aa3da02e905aa346ee30
Im trying to figure out how to call functions based on what a user clicks on a form. But im not sure if im doing it right.
I have a number of classes, lets say 3 for different ways to connect to a site, the user clicks on which one they would like.
FTP
SFTP
SSH
Which i have named 'service' in my code.
I don't want to run a whole bunch of IF statements, i would rather try and build the call dynamically.
What i have at the moment is as follows
$ftp_backup = new FTPBackup;
$sftp_backup = new SFTPBackup;
$ssh_backup = new SSHBackup;
$service = $request->input('service') . '_backup';
$service->testConn($request);
Im getting the following error
Call to a member function testConn() on string
Im not sure im doing this right.
Any help would be greatly appreciated.
Thanks
First of all $service is a string on which You cannot call method, because it is not an object (class instance).
I think it is a great example of where You can use Strategy Pattern which look like that:
class BackupStrategy {
private $strategy = null;
public function __construct($service_name)
{
switch ($service_name) {
case "ftp":
$this->strategy = new FTPBackup();
break;
case "sftp":
$this->strategy = new SFTPBackup();
break;
case "ssh":
$this->strategy = new SSHBackup();
break;
}
}
public function testConn()
{
return $this->strategy->testConn();
}
}
And then in place where You want to call it You call it by:
$service = new BackupStrategy($request->input('service'));
$service->testConn($request);
I suggest You to read about Design Patterns in OOP - it will help You a lot in the future.
How about this:
$ftp_backup = new FTPBackup;
$sftp_backup = new SFTPBackup;
$ssh_backup = new SSHBackup;
$service = $request->input('service') . '_backup';
${$service}->testConn($request);
This is called "Variables variable": http://php.net/manual/en/language.variables.variable.php
// Create class name
$className = $request->get('service') . '_backup';
// Create class instance
$service = new $className();
// Use it as you want
$service->testConn($request);
For example in a Yii Framework application the url is in this format
www.example.com/index.php?r=foo/bar
Which renders the script inside the actionBar() method of class FooController. Further, this class (or its parent class) implements a render() method which can render a view file.
All the url's are handled through the entry script index.php.
I would like to write my own class which can handle url's through this way.
Can someone give me a very basic 'hello world' example of writing such a script ?
I'll give it a shot:
// index.php
$r = $_REQUEST['r']; // 'foo/bar'
$rParts = explode('/',$r);
$foo = $rParts[0];
$bar = $rParts[1];
$controller = new $foo; // foo
echo $controller->$bar();
Here is what I did for a friend recently, when teaching him how frameworks works. This is a basic example, but it demonstrates how a container works, how to handle the router, giving the controller a request and a response and handling redirects and the like.
<?php
require 'autoload.php';
$container = [];
$container['controller.elephant'] = function() {
return new Controller\Elephant();
};
$routes = [];
$routes['/babar'] = 'controller.elephant:babar';
$routes['/celeste'] = 'controller.elephant:celeste';
$request = new Request();
if (!isset($routes[$request->path()])) {
http_response_code(404);
exit;
}
$route = $routes[$request->path()];
list($class, $method) = explode(':', $route);
$controller = $container[$class]();
$response = $controller->{$method}($request, new Response());
if ($response->isRedirect()) {
http_response_code($response->status());
header('Location: '.$response->destination());
} else {
echo $response->content();
}
exit;
I won't include anything more than that (albeit there is other files) because it would bloat the answer needlessly (I can send them to you by other means if you want to).
I highly advise you to look at the Slim Framework code, as it is a micro framework that basically do just that.
Well in the Symfony documentation you have this page: http://symfony.com/doc/current/components/http_kernel/introduction.html
where it explains how is the life cycle of a request, it's just a flow diagram.
But it will give you a really good idea on how you should build yours
If you are more interested in how based on a url you get the controller you should read the RoutingComponent in symfony
http://symfony.com/doc/current/components/routing/introduction.html
http://symfony.com/doc/current/components/routing/hostname_pattern.html
But if you want to write your own class, you should use something like regex expression groups where you can detect the url parts separated by i.e: '/' then you somehow map the url to the controller i.e associative array 'Hash'
someurl.com/someController/someAction
$mappings = [
...
'someController' => 'The\Controller\Class'
]
$controller = new $mappings[$urlControllerPart]();
$response = $controller->{$urlActionPart}($request);
return $response;
How can I pass a dynamic dependency from one registered container definition to another? In this case, a generic Database object wants to inherit from a generic Config object. The twist is config is not static, but loaded depending on a given environment variable.
Config pertinent methods
public function __construct()
{
$configFile = 'example.config.yml';
$yamlParser = new Parser();
$reader = new Config\Reader($yamlParser);
$configYaml = $reader->parse(file_get_contents($configFile));
$config = new Config\Environment(getenv('SITE'), $configYaml);
$this->config = $config;
}
public function getEnvironmentConfig()
{
return $this->config;
}
Registering config is as simple as
$container->register('config', 'Config');
Database is currently added to the container as follows:
$container
->register('database', 'Database')
->addArgument($config->getEnvironmentConfig('Database', 'db.username'))
->addArgument($config->getEnvironmentConfig('Database', 'db.password'))
;
But I want to do something like
$container
->register('database', 'Database')
->addArgument(new Reference('config')->getEnvironmentConfig('Database', 'db.username'))
->addArgument(new Reference('config')->getEnvironmentConfig('Database', 'db.password'))
;
The $config in-PHP variable makes migrating from a PHP-built config impossible. I want to define the services in yaml force the container to:
Instantiate Config
Parse the config yaml file and create an environment-specific version
Return this on a call to getEnvironmentConfig
Is this possible?
This was solved by using the Expression Language Component
So you can easily chain method calls, for example:
use Symfony\Component\ExpressionLanguage\Expression;
$container->register('database', 'Database')
->addArgument(new Expression('service("config").getEnvironmentConfig("Database", "db.username")'));