How to inject framework into custom class? - php

I'm currently developing an application with the FlightPHP framework and wondering how am I able to inject FlightPHP into my custom class so that I am able to use specific classes I have injected into it's dependency container.
use Flight;
use Logger;
class DB{
public function __construct(...){
$this->app = $app; // Flight:: instance
}
public function doStuff($stuff){
return $this->app->log()->function($stuff);
}
}
Flight::register('log', 'Logger', ['app'], function($log) {
return $log->pushHandler(new StreamHandler('app.log'));
});
Flight::register('database', 'DB', array($data), function($db) {
return $db;
});
I'm attempting to inject Flight into my database class constructor so that I am able to use the log function which was previously injected into the Flight dependency container.
The "Logger" works in the index.php when used under the Flight instance "Flight::log()->function("test");", however when I attempt to use it in another scope(within the Database class), it doesn't allow me to use it in the context of "Flight".
Update:
Flight::register('log', 'Monolog\Logger', ['app'], function($log) {
return $log->pushHandler(new StreamHandler('app.log'));
});
class DB{
function __construct(Monolog\Logger $engine){
#var_dump($engine);
$engine->addInfo("injected"); // works
}
}
Flight::register('database', 'DB', array(Flight::log()), function($db) {
return $db;
});
Flight::database();
Is correct usage?

You could pass instance of \Flight\Engine in the array of third parameter at register method to pass framework instance in you DB controller. \Flight\Engine does not use interface sou you are coupling your code with framework implementation I guess. In this case you can use Flight::app() everywhere to obtain framework instance.
<?php error_reporting(E_ALL);
require 'vendor/autoload.php';
class DB
{
function __construct(\Flight\Engine $engine)
{
var_dump($engine->get('connectionString'));
}
}
Flight::set('connectionString', 'mssql');
Flight::register('database', 'DB', array(Flight::app()), function($db) {
return $db;
});
Flight::database();
Looks like that Flight does not have such a concept as Dependency Injection Container. You have to specify your parameter values explicitly.
Update:
By doing this ['app'] you are injecting string into constructor of Monolog\Logger. This line return $log->pushHandler(new StreamHandler('app.log')); should raise error.
Read more carefully http://flightphp.com/learn

Related

Injecting in the constructor when using pimple

Background. I am using Slim where an ID is either in the endpoint or parameters. Based on the ID, a factory creates the appropriate object to perform the needed action.
I have a service which needs some data obtained in the request injected into it. I could therefore do the following:
//index.php
require '../vendor/autoload.php';
use Pimple\Container;
$container = new Container();
class SomeService
{
private $dataFromTheRequest;
public function __construct($dataFromTheRequest){
$this->dataFromTheRequest=$dataFromTheRequest;
echo($dataFromTheRequest);
}
}
$dataFromTheRequest='stuff1';
$container['someService1'] = function ($c) use($dataFromTheRequest) {
return new SomeService($dataFromTheRequest);
};
$someService=$container['someService1'];
But the service is not used in index.php where the service is defined but in another class, so i can do the following:
class SomeOtherClass1
{
public function someMethod($dataFromTheRequest){
$someService=new SomeService($dataFromTheRequest);
}
}
$someOtherClass1=new SomeOtherClass1();
$someOtherClass1->someMethod('stuff2');
But I want to use the instance assigned in index.php, so I can do the following:
$container['someService2'] = function ($c) {
return new SomeService($c['dataFromTheRequest']);
};
class SomeOtherClass2
{
public function __construct($container){
$this->container=$container;
}
public function someMethod($dataFromTheRequest){
$this->container['dataFromTheRequest']='stuff3';
$someService=$this->container['someService2'];
}
}
$someOtherClass2=new SomeOtherClass2($container);
$someOtherClass2->someMethod();
But using the container to pass data just seems wrong.
How should data be injected in a pimple service if that data is not known when the service is defined?

PHP DI pattern for function driven application

I have some classes that require dependencies injected into their constructors. This allows me to inject mocks (e.g. from prophecy) for testing.
I'm interested in using a container to help configure and access these objects, and I've looked at Pimple for this (I also looked at PHP-DI although I couldn't get that to resolve stuff on a quick attempt).
All good so far. BUT, the problem I have is that the application (Drupal 7) is built around thousands of functions which do not belong to an object that can have dependencies injected into.
So I need these functions to be able to access the services from the container. Further more, for testing purposes, I need to replace the services with mocks and new mocks.
So the pattern is like:
<?php
/**
* Some controller class that uses an injected mailing service.
*/
class Supporter
{
protected $mailer;
public function __construct(MailingServiceInterface $mailer) {
$this->mailer = $mailer;
}
public function signUpForMalings($supporter_id) {
$email = $this->getSupporterEmail($supporter_id);
$this->mailer->signup($email);
}
}
Then peppered in various functions I'd use:
<?php
/**
* A form submit handler called by the platform app,
* with a signature I can't touch.
*/
function my_form_submit($values) {
global $container;
if ($values['subscribe']) {
$supporter = $container->get('supporter');
$supporter->signUpForMailings($values['supporter_id']);
}
}
Elsewhere I may need to access the mailer directly...
<?php
/**
* example function requires mailer service.
*/
function is_signed_up($email) {
global $container;
return $container->get('mailer')->isSignedUp($email);
}
And elsewhere a function that calls those functions...
<?php
/**
* example function that uses both the above functions
*/
function sign_em_up($email, $supporter_id) {
if (!is_signed_up($email)) {
my_form_submit(['supporter_id'=>$supporter_id);
return TRUE;
}
}
Let's acknowledge that these functions are a mess - that's a deliberate representation of the problem. But let's say I want to test the sign_em_up function:
<?php
public testSignUpNewPerson() {
$mock_mailer = createAMockMailer()
->thatWill()
->return(FALSE)
->whenFunctionCalled('isSignedUp', 'wilma#example.com');
// Somehow install the mock malier in the container.
$result = sign_em_up('wilma#example.com', 123);
$this->assertTrue($result);
}
// ... imagine other tests which also need to inject mocks.
While I recognise that this is using the container as a Service Locator in the various global functions, I think this is unavoidable given the nature of the platform. If there's a cleaner way, please let me know.
However my main question is:
There's a problem with injecting mocks, because the mocks need to change for various tests. Lets say I swap out the mailer service (in Pimple: $container->offsetUnset('mailer'); $container['mailer'] = $mock_mailer;), but if Pimple had already instantiated the supporter service, then that service will have the old, unmocked mailer object. Is this a limitation of the containter software, or the general container pattern, or am I Doing It Wrong, or is it just a mess because of the old-school function-centred application?
Here's what I've gone for, in absence of any other suggestions!
Container uses Pimple\Psr11\ServiceLocator
I'm using Pimple, so the container's factories may look like this
<?php
use Pimple\Container;
use Pimple\Psr11\ServiceLocator;
$container = new Container();
$container['mailer'] = function ($c) { return new SomeMailer(); }
$container['supporters'] = function ($c) {
// Create a service locator for the 'Supporters' class.
$services = new ServiceLocator($c, ['mailer']);
return new Supporter($services);
}
Then the Supporter class now instead of storing references to the objects extracted from the container when it was created, now fetches them from the ServiceLocator:
<?php
use \Pimple\Psr11\ServiceLocator;
/**
* Some controller class that uses an injected mailing service.
*/
class Supporter
{
protected $services;
public function __construct(ServiceLocator $services) {
$this->services = $services;
}
// This is a convenience function.
public function __get($prop) {
if ($prop == 'mailer') {
return $this->services->get('mailer');
}
throw new \InvalidArgumentException("Unknown property '$prop'");
}
public function signUpForMalings($supporter_id) {
$email = $this->getSupporterEmail($supporter_id);
$this->mailer->signup($email);
}
}
In the various CMS functions I just use global $container; $mailer = $container['mailer'];, but it means that that in tests I can now mock any service and know that all code that needs that service will now have my mocked service. e.g.
<?php
class SomeTest extends \PHPUnit\Framework\TestCase
{
function testSupporterGetsMailed() {
global $container;
$supporter = $container['supporter'];
// e.g. mock the mailer component
$container->offsetUnset('mailer');
$container['mailer'] = $this->getMockedMailer();
// Do something with supporter.
$supporter->doSomething();
// ...
}
}

Injecting parameters into constructor with PHP-DI

I am struggling to get dependency injection to work the way I expect -
I am trying to inject a class, Api, which needs to know which server to connect to for a particular user. This means that overriding constructor properties in a config file is useless, as each user may need to connect to a different server.
class MyController {
private $api;
public function __construct(Api $api) {
$this->api = $api;
}
}
class Api {
private $userServerIp;
public function __construct($serverip) {
$this->userServerIp = $serverip;
}
}
How can I inject this class with the correct parameters? Is it possible to override the definition somehow? Is there some way of getting the class by calling the container with parameters?
To (hopefully) clarify - I'm trying to call the container to instantiate an object, while passing to it the parameters that would otherwise be in a definition.
Since IP depends on the user you probably have some piece of logic that does the user=>serverIP mapping. It might be reading from the db or simple id-based sharding, or whatever. With that logic you can build ApiFactory service that creates Api for a particular user:
class ApiFactory {
private function getIp(User $user) {
// simple sharding between 2 servers based on user id
// in a real app this logic is probably more complex - so you will extract it into a separate class
$ips = ['api1.example.com', 'api2.example.com'];
$i = $user->id % 2;
return $ips[$i];
}
public function createForUser(User $user) {
return new Api($this->getIp($user);
}
}
Now instead of injecting Api into your controller you can inject ApiFactory (assuming your controller knows the user for which it needs the Api instance)
class MyController {
private $apiFactory;
public function __construct(ApiFactory $apiFactory) {
$this->apiFactory = $apiFactory;
}
public function someAction() {
$currentUser = ... // somehow get the user - might be provided by your framework, or might be injected as well
$api = $this->apiFactory->createForUser($currentUser);
$api->makeSomeCall();
}
}
I am not sure I understand your question fully, but you can configure your Api class like this:
return [
'Foo' => function () {
return new Api('127.0.0.1');
},
];
Have a look at the documentation for more examples or details: http://php-di.org/doc/php-definitions.html
Edit:
return [
'foo1' => function () {
return new Api('127.0.0.1');
},
'foo2' => function () {
return new Api('127.0.0.2');
},
];

Proper way to deal with class dependency in a custom class in Laravel 4

I'm adding a custom class to my Laravel library, and seem to be very close to success. Heres what I've done so far, followed by the error I'm getting:
My class is called Connector and is located in Acme/Api/Zurmo/Connector
This class requires another class, so this is the code for that:
use Acme\Api\Rest\ApiRestHelper;
class Connector implements ConnectorInterface {
protected $rest;
public function __construct(ApiRestHelper $rest)
{
$this->rest = $rest;
}
and my ApiRestHelper class starts like this:
namespace Acme\Api\Rest;
class ApiRestHelper {
Then I've just added a quick closure in routes.php to test this works, (which it doesn't):
Route::get('/', function()
{
$connector = new Acme\Api\Zurmo\Connector;
var_dump($connector);
});
This is the error I get:
Argument 1 passed to Acme\Api\Zurmo\Connector::__construct() must be
an instance of Acme\Api\Rest\ApiRestHelper, none given
I first assumed I'd screwed my namespacing, filenaming up, but Laravel can locate the class as I can do the following without error:
$rest = new Acme\Api\Rest\ApiRestHelper;
var_dump($rest);
Any ideas what I'm missing here? Thank you.
The constructor expects you to pass in an instance of resthelper:
//manual dependency injection
Route::get('/', function()
{
$connector = new Acme\Api\Zurmo\Connector(new Acme\Api\Rest\ApiRestHelper);
var_dump($connector);
});
OR change the constructor
//creates a hard dependency
use Acme\Api\Rest\ApiRestHelper;
class Connector implements ConnectorInterface {
protected $rest;
public function __construct()
{
$this->rest = new ApiRestHelper();
}
}
A more advanced option would be to use the IOC container for dependency inject, but thats beyond the scope of this answer
http://laravel.com/docs/ioc

Constructor Injection in Restler

Is it possible to do pass along arguments to an API class for the sake of constructor injection? For example, in index.php I have the following:
$r->addAPIClass('Contacts', '');
And Contacts.php looks something like this:
class Contacts
{
private $v;
public function __construct(Validation v)
{
$this->v = v;
}
}
How would I do this with Restler?
Restler 3 RC5 has a dependency injection container called Scope which is responsible for creating new instances of any class from their name, it comes in handy for this purpose.
Once you register the Contacts class with its dependency using the register method, it will be lazy instantiated when asked for
<?php
include '../vendor/autoload.php';
use Luracast\Restler\Scope;
use Luracast\Restler\Restler;
Scope::register('Contacts', function () {
return new Contacts(Scope::get('Validation'));
});
$r = new Restler();
$r->addAPIClass('Contacts', '');
$r->handle();
By using Scope::get('Validation') we can register Validation too if it has any dependencies

Categories