I am having issues getting dependency injection to work with dependency inversion. I can call App::make in the constructor, and the dependency inversion works just fine ... it is only when I try to inject it into the constructor that I have issues.
ReflectionException Class StationsInterface does not exist
A uri that would hit this would be .../stats/station/mogreet/6
The File Stucture:
-app
-controllers
*StatsController
-Dashboard
-Datasync
-Interfaces
*DatasyncInterface
*MogreetDatasyncInterface
-Services
*MogreetDatasync
*BackendServiceProvider
*DatasyncBase
-Repositories
-Interfaces
*CredentialsInterface
*StationsInterface
-Stations
*DbCredentials
*DbStations
*FileCredentials
-Stats
*BackendServiceProvider
*DbRepositoryBase
The relevant code blocks are as follows:
Service Provider:
<?php namespace Dashboard\Repositories;
use Illuminate\Support\ServiceProvider;
class BackendServiceProvider extends ServiceProvider {
public function register() {
// Service Providers located in Stats directory
$this->app->bind('StatsDailyInterface', 'Dashboard\Repositories\Stats\DbStatsDaily');
//$this->app->bind('StatsMonthlyRepository', 'Dashboard\Repositories\Stats\DbStatsMonthly');
//$this->app->bind('StatsYearlyRepository', 'Dashboard\Repositories\Stats\DbStatsYearly');
// Service Providers located in Stations directory
$this->app->bind('CredentialsInterface', 'Dashboard\Repositories\Stations\FileCredentials');
$this->app->bind('StationsInterface', 'Dashboard\Repositories\Stations\DbStations');
}
}
Controller: Note that in the constructor of this I am using App::make instead of Injecting the Dependency. If I inject the dependency I get a class resolution error exactly like I do in the DatasyncBase class.
<?php
use Dashboard\ConrollerFacades\Facades\Services;
class StatsController extends BaseController {
/*
|--------------------------------------------------------------------------
| Stats Controller
|--------------------------------------------------------------------------
|
| Pull and display stats for station, market, or corporate views
|
|
|
*/
private $StationModel;
public function __construct() {
$this->StationModel = App::make('StationsInterface');
}
/**
* Pulls stats for an individual station
*
* #param string $service of station
* #param integer $id of station
*
* #return void
*/
public function station( $service, $stationId ) {
$this->Service = $this->serviceSelector($service);
if(!$this->Service) throw new Exception('Unknown Service Selected', 1);
$this->Service->init($stationId);
exit();
}
/**
* Pulls stats for a Market
*
* #param integer $id of market
*
* #return void
*/
public function market( $service, $marketId ) {
$this->Service = $this->serviceSelector($service);
if(!$this->Service) throw new Exception('Unknown Service Selected', 1);
foreach($StationModel->getStationIdsByMarket($marketId) as $station) {
$this->Service->init($station);
}
exit();
}
/**
* Pulls stats for Corporate (all stations)
*
* #return void
*/
public function corporate( $service ) {
$this->Service = $this->serviceSelector($service);
if(!$this->Service) throw new Exception('Unknown Service Selected', 1);
foreach($StationModel->getAllStationIds() as $station) {
$this->Service->init($station);
}
exit();
}
private function serviceSelector($service) {
switch(strtolower($service)) {
case 'brightcove': return App::make('BrightCoveDatasyncInterface'); break;
case 'facebook': return App::make('FacebookDatasyncInterface'); break;
case 'googleanalytics': return App::make('GoogleAnalyticsDatasyncInterface'); break;
case 'liquidcompass': return App::make('LiquidCompassDatasyncInterface'); break;
case 'mogreet': return App::make('MogreetDatasyncInterface'); break;
case 'twitter': return App::make('TwitterDatasyncInterface'); break;
default: return false; break;
}
}
}
The constructor of this class is where the dependency injection issue is occurring.
DatasyncBase: This class is never directly instantiated, it is inherited by a service class like MogreetDatasync. Moving the constructor to the MogreetDatasync class for testing does not resolve the issue.
<?php namespace Dashboard\Datasync;
use Dashboard\Repositories\Interfaces\StationsInterface;
use Dashboard\Repositories\Interfaces\CredentialsInterface;
class DatasyncBase {
protected $Station;
protected $Credentials;
protected $results;
protected $stats;
public function __construct(StationsInterface $Station , CredentialsInterface $Credentials) {
$this->Station = $Station;
$this->Credentials = $Credentials;
$this->stats = array();
}
public function __destruct() {
unset($this->results);
unset($this->stats);
}
public function init() {}
protected function fetch($uri = null, $post_fields = null) {
$cURL = curl_init();
curl_setopt($cURL, CURLOPT_URL, $uri);
curl_setopt($cURL, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($cURL, CURLOPT_POSTFIELDS, $post_fields);
$this->results = curl_exec($cURL);
curl_close($cURL);
}
}
A Datasync Service:
<?php namespace Dashboard\Datasync\Services;
use Dashboard\Datasync\DatasyncBase;
use Dashboard\Datasync\Interfaces\MogreetDatasyncInterface;
class MogreetDatasync extends DatasyncBase implements MogreetDatasyncInterface {
public function init($stationId) {}
protected function uri() {}
protected function parse() {}
protected function write() {}
}
The answer to this question is closures for the ServiceDatasyncInterfaces. Previously I was defining the bindings like this:
$this->app->bind('MogreetDatasyncInterface', 'Dashboard\Datasync\Services\MogreetDatasync');
This however does NOT allow the IoC to "recursively" inject the Inversion Dependencies, you must use App::make('InversionInterface') for IoC to actually resolve this correctly.
<?php namespace Dashboard\Datasync;
use Illuminate\Support\ServiceProvider;
use Dashboard\Datasync\Services\BrightCoveDatasync;
use Dashboard\Datasync\Services\FacebookDatasync;
use Dashboard\Datasync\Services\GoogleAnalyticsDatasync;
use Dashboard\Datasync\Services\LiquidCompassDatasync;
use Dashboard\Datasync\Services\MogreetDatasync;
use Dashboard\Datasync\Services\TwitterDatasync;
class BackendServiceProvider extends ServiceProvider {
public function register() {
$this->app->bind('BrightCoveDatasyncInterface', function() { return new BrightCoveDatasync( $this->app->make('StationsInterface'), $this->app->make('CredentialsInterface') ); });
$this->app->bind('FacebookDatasyncInterface', function() { return new FacebookDatasync( $this->app->make('StationsInterface'), $this->app->make('CredentialsInterface') ); });
$this->app->bind('GoogleAnalyticsDatasyncInterface', function() { return new GoogleAnalyticsDatasync( $this->app->make('StationsInterface'), $this->app->make('CredentialsInterface') ); });
$this->app->bind('LiquidCompassDatasyncInterface', function() { return new LiquidCompassDatasync( $this->app->make('StationsInterface'), $this->app->make('CredentialsInterface') ); });
$this->app->bind('MogreetDatasyncInterface', function() { return new MogreetDatasync( $this->app->make('StationsInterface'), $this->app->make('CredentialsInterface') ); });
$this->app->bind('TwitterDatasyncInterface', function() { return new TwitterDatasync( $this->app->make('StationsInterface'), $this->app->make('CredentialsInterface') ); });
}
}
This is a rather minor issue, but you will need to use the correct interface in the file containing the class that is being injected. My DatasyncBase file now looks like this:
<?php namespace Dashboard\Datasync;
use Dashboard\Repositories\Interfaces\StationsInterface;
use Dashboard\Repositories\Interfaces\CredentialsInterface;
class DatasyncBase {
protected $Station;
protected $Credentials;
protected $results;
protected $stats;
public function __construct(StationsInterface $Station, CredentialsInterface $Credentials) {
$this->Station = $Station;
$this->Credentials = $Credentials;
$this->stats = array();
}
public function __destruct() {
unset($this->results);
unset($this->stats);
}
public function init() {}
protected function fetch($uri, $post_fields = '') {
$cURL = curl_init();
curl_setopt($cURL, CURLOPT_URL, $uri);
curl_setopt($cURL, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($cURL, CURLOPT_POSTFIELDS, $post_fields);
$this->results = curl_exec($cURL);
curl_close($cURL);
}
}
You can find more on ServiceProvider's here:
https://laracasts.com/lessons/service-providers-decoded
Related
Was testing an application and was repeatable getting the infamous new entity not configured to cascade persist error. I was surprised since I wasn't even creating new entities, and after digging into it, it appears to be relate to using different instances of the EntityManager object (I have confirmed that they are working with the same database, however) which I guess makes sense since each test will have a transaction applied. The only way I was able to get rid of the errors was to use the entityManager in the container instead of the autowired ones. While it works, it is a bit of a kludge and I would like to know the right way of doing this. Thank you
namespace App\Tests;
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
class MyTest extends ApiTestCase
{
/**
* #dataProvider getData
*/
public function testWhichDoesNotWork(int $id, string $class)
{
$service = static::getContainer()->get(MyService::class);
$user = $service->getUser();
$randomEntity = $service->getRandomEntity($user->getTenant(), $class);
$randomEntity->setSomething('something');
$service->saveEntity($randomEntity);
}
/**
* #dataProvider getData
*/
public function testWhichWorks(int $id, string $class)
{
$service = static::getContainer()->get(MyService::class);
$em = static::getContainer()->get(EntityManagerInterface::class);
$user = $service->getUser();
$randomId = $service->getRandomEntityId($user->getTenant(), $class);
$randomEntity = $em->getRepository($class)->find($randomId);
$randomEntity->setSomething('something');
$em->persist($randomEntity);
$em->flush();
}
/**
* #dataProvider getData
*/
public function testAnotherWhichWorks(int $id, string $class)
{
$service = static::getContainer()->get(MyService::class);
$service->setNewEntityManager(static::getContainer()->get(EntityManagerInterface::class));
$user = $service->getUser();
$randomEntity = $service->getRandomEntity($user->getTenant(), $class);
$randomEntity->setSomething('something');
$service->saveEntity($randomEntity);
}
public function getData(): array
{
return [
[123, SomeClass::class]
];
}
}
namespace App\Test\Service;
final class MyService
{
public function __construct(private EntityManagerInterface $entityManager)
{}
public function setNewEntityManager(EntityManagerInterface $entityManager):self
{
$this->entityManager = $entityManager;
return $this;
}
public function getDatabase():string
{
return $this->entityManager->getConnection()->getDatabase();
}
public function getUser(int $id):User
{
return $this->entityManager->getRepository(User::class)->find($id);
}
public function getRandomId(Tenant $tenant, string $class):int
{
$meta = $this->entityManager->getClassMetadata($class);
$_sql = 'SELECT %s FROM public.%s WHERE tenant_id=? OFFSET floor(random() * (SELECT COUNT(*) FROM public.%s WHERE tenant_id=?)) LIMIT 1;';
$sql = sprintf($_sql, $meta->getSingleIdentifierFieldName(), $meta->getTableName(), $meta->getTableName());
return $this->entityManager->getConnection()->prepare($sql)->execute([$tenant->getId(), $tenant->getId()])->fetchOne();
}
public function getRandomEntity(Tenant $tenant, string $class):object
{
return $this->entityManager->getRepository($class)->find($this->getRandomId($tenant, $class));
}
public function saveEntity(object $entity):self
{
$this->entityManager->persist($entity);
$this->flush();
return $this;
}
}
services:
app.test.my.service:
alias: App\Test\Service\MyService
public: true
I'm trying to create a class function which resembles how we used to fetch database listing and convert into a dropdown listing.
eg: DB::table()->where()->get()
what i would like to achieve in laravel custom class or through model is this
Dropdown::fetch()->toArray()
Dropdown::fetch()->toDropdown()
I tried to figure out how this can be done through google. But couldn't find any solution to it.
I'm using laravel 5.8
--
Edit - Sample Code added
Code tried:
namespace App\Http\Models;
use DB;
use Closure;
use BadMethodCallException;
use Illuminate\Support\Traits\Macroable;
use Illuminate\Database\Eloquent\Model;
class Dropdown extends Model
{
private $result = [];
private $default;
public function _cities(){
$tbl_cities = config("tables.TBL_meta_cities");
$result = DB::table($tbl_cities)->select('id', 'cityname')
->orderBy('id')->get()->toArray();
$this->result = $result;
}
public function _select(){
}
public function _list(){
return $this->result;
}
public function _setDefault($def=''){
}
public static function __callStatic($method, $parameters)
{
$action = '_'.$method;
if(method_exists(get_called_class(), $action))
self::$action(...$parameters);
else echo 'not found';
}
public function __call($method, $parameters)
{
$action = '_'.$method;
if(method_exists($get_called_class(), $action))
self::$action(...$parameters);
else echo 'not found';
}
}
and i tried
Dropdown::cities()->list()
but ended with bugs
Well i figured it out myself.
class Dropdown extends Model
{
private static $result = [];
private function getCities(){
$result = City::select('id', 'cityname')
->orderBy('id')->get()->toArray();
self::$result = $result;
}
public function toArray(){
return self::$result;
}
public function toDropdown(){
// Do the dropdown works
}
/**
* Dynamically handle calls to the class.
*
* #param string $method
* #param array $parameters
* #return mixed
*
* #throws \BadMethodCallException
*/
public function __callMethod($method, $parameters){
// Check with inclusive
$class = get_called_class();
$avail = false;
$action = '';
// Check method availability - direct
if(!$avail){
$action = $method;
$avail = method_exists($class, $action);
}
// Check method 2
if(!$avail){
$action = 'get'.ucwords($method);
$avail = method_exists($class, $action);
}
if($avail){
// Call the method
$return = self::$action(...$parameters);
if(!empty($return)) return $return;
} else {
// Throw error if method not found
throw new BadMethodCallException("No such method exists: $name");
}
return new self;
}
public static function __callStatic($method, $parameters){
return (new self)->__callMethod($method, $parameters);
}
public function __call($method, $parameters){
return (new self)->__callMethod($method, $parameters);
}
}
All i need to do is return new self which does the trick instead of return $this so that the trailing function can be called easily.
Now i can able to call that function like this
Dropdown::cities()->toArray();
Reference:
https://stackoverflow.com/a/41631711/1156493
Thank you #Joseph for your time & support.
Using Symfony 4.4 with autowiring activated, I want to instantiate a class using the design-pattern FactoryMethod.
The class instantiated is a service with autowired arguments passed into the constructor.
It work well if the constructor is the same for each type of class to instantiate inside the factory method.
But, each service to instantiate has to autowire some specific service in order to work.
I found that we could use the "setter dependency injection". Articles describing it:
https://symfonycasts.com/screencast/symfony-fundamentals/logger-trait
https://symfony.com/doc/4.4/service_container/injection_types.html#setter-injection
I tried to implement the setter dependency injection but the code inside is never executed.
Considering the articles, we should enter the setters with the PHPDoc "#required" immediately after the __construct method has been called (from what I understood).
It doesn't work with my code (see below).
Is my implementation correct?
Is there a better way of doing it?
My code looks like:
// Controller
/**
*#Route("/my_action/{param}")
*/
public function my_action (ThingManagerFactory $thingManagerFactory, $param)
{
$thingManager = $thingManagerFactory->get($param);
$thingManager->doSomething();
}
// ThingManagerFactory
class ThingManagerFactory
{
private $firstManager;
private $secondManager;
private $thirdManager;
public function __construct(FirstManager $firstManager, SecondManager $secondManager, ThirdManager $thirdManager)
{
$this->firstManager = $firstManager;
$this->secondManager = $secondManager;
$this->thirdManager = $thirdManager;
}
public function get($param): ThingManagerInterface
{
if($param == 1) {
return new Thing1Manager(
$this->firstManager,
$this->secondManager,
$this->thirdManager,
);
} elseif($param == 2) {
return new Thing2Manager(
$this->firstManager,
$this->secondManager,
$this->thirdManager,
);
}
throw new \InvalidArgumentException("...");
}
}
// ThingManagerInterface
interface ThingManagerInterface
{
public function __construct(
$this->firstManager,
$this->secondManager,
$this->thirdManager,
);
public function doSomething();
}
// Thing1Manager
class Thing1Manager implements ThingManagerInterface
{
(...)
private $spec1Manager;
public function __construct(
$this->firstManager,
$this->secondManager,
$this->thirdManager,
)
{
(...)
}
/**
* #required
*/
public function setSpecificManager(Spec1Manager $spec1Manager)
{
// this code is never called
$this->spec1Manager = $spec1Manager;
}
public function doSomething()
{
// we pass here before going into setSpecificManager
(...)
}
}
// Thing2Manager class
// is similar to Thing1Manager with multiple other specific managers.
Thank you for your help.
In order to use the design-pattern Factory Method with Symfony, use the Service Locator to provide autowire outside a Controller.
Refactor the code to the following:
// Controller
/**
*#Route("/my_action/{param}")
*/
public function my_action (ThingManagerFactory $thingManagerFactory, $param)
{
$thingManager = $thingManagerFactory->get($param);
$thingManager->doSomething();
}
// ThingManagerFactory
use App\Locator\ThingLocator;
class ThingManagerFactory
{
private $locator;
public function __construct(ThingLocator $locator)
{
$this->locator = $locator;
}
public function get($param): ThingManagerInterface
{
if($param == 1) {
return $this->locator->get(Thing1Manager::class);
} elseif($param == 2) {
return $this->locator->get(Thing2Manager::class);
}
throw new \InvalidArgumentException("...");
}
}
// ServiceLocatorInterface
interface ServiceLocatorInterface
{
public function get(string $id);
}
// ThingLocator
class ThingLocator implements ServiceLocatorInterface, ServiceSubscriberInterface
{
private $locator;
public function __ construct(ContainerInterface $locator)
{
$this->locator = $locator;
}
public function get(string $id)
{
if (!$this->locator->has($id)) {
throw new \Exception("The entry for the given '$id' identifier was not found.");
}
try {
return $this->locator->get($id);
} catch (ContainerExceptionInterface $e) {
throw new \Exception("Failed to fetch the entry for the given '$id' identifier.");
}
}
public static function getSubscribedServices()
{
return [
Thing1Manager::class,
Thing2Manager::class,
];
}
}
// ThingManagerInterface
interface ThingManagerInterface
{
public function doSomething();
}
// Thing1Manager
class Thing1Manager implements ThingManagerInterface
{
// ...
private $spec1Manager;
public function __construct($firstManager, $secondManager, $thirdManager, $spec1Manager)
{
// ...
}
// This setter is no more needed. This manager can be added to the constructor method.
// **
// * #required
// */
//public function setSpecificManager(Spec1Manager $spec1Manager)
//{
// if not commented, this code would be called thanks to the Service Locator (which is a Symfony Service Container)
// $this->spec1Manager = $spec1Manager;
//}
public function doSomething()
{
// ...
}
}
I started using mockery so I have a problem in doing my unit test . I want to test authenticate middleware , I passed one condition for expectsJson so I need one more pattern to return true from expectesJson like below but not success
Authenticate.php
protected function redirectTo($request)
{
if (! $request->expectsJson()) {
return route('login');
}
}
AuthenticatTest.php
class AuthenticateTest extends TestCase
{
/**
* A basic unit test example.
*
* #return void
*/
public function testMiddleware()
{
$request = Request::create(config('app.url') . '500', 'GET',[],[],[],['REMOTE_ADDR'=>'127.0.0.1:8000']);
$middleware = new Authenticate($this->createMock(Factory::class));
$class = new \ReflectionClass(Authenticate::class);
$method = $class->getMethod("redirectTo");
$method->setAccessible(true);
$expectedStatusCode = 401;
$this->assertContains("http://",$method->invokeArgs($middleware,[$request]));
}
public function testMiddlewareElse()
{
$this->mock(Request::class, function($mock) {
$mock->shouldReceive("expectsJson")
->once()->andReturn(true);
});
$request = Request::create(config('app.url') . '200', 'POST',[],[],[],['REMOTE_ADDR'=>'127.0.0.1:00']);
$middleware = new Authenticate($this->createMock(Factory::class));
$class = new \ReflectionClass(Authenticate::class);
$method = $class->getMethod("redirectTo");
$method->setAccessible(true);
$this->assertContains("",$method->invokeArgs($middleware,[$request]));
}
}
testMiddlewareElse is failed , How to return true for $request->expectsJson
Here's how you could test a request for the authentication middleware. Assume that you have a route that requires authentication that is managed by UserController#dashboard (or similar):
public function testMiddleware() {
// You could disable the other middleware of the route if you don't want them to run e.g.
// $this->withoutMiddleware([ list of middleware to disable ]);
$mockController = $this->prophecy(UserController::class);
//This is if the middleware passes and the controller method is called, use shouldNotBeCalled if you expect it to fail
$mockController->dashboard(Prophecy::any())->shouldBeCalled();
$this->app->instance(
UserController::class,
$mockController->reveal()
);
$this->json("GET", url()->action("UserController#dashboard"));
}
I found the solution ! I need to pass mock class in invoke params ...;)
public function testMiddlewareElse()
{
$mock = $this->mock(Request::class, function($mock) {
$mock->shouldReceive("expectsJson")
->once()->andReturn(true);
});
$request = Request::create(config('app.url') . '200', 'POST',[],[],[],['REMOTE_ADDR'=>'127.0.0.1:00']);
$middleware = new Authenticate($this->createMock(Factory::class));
$class = new \ReflectionClass(Authenticate::class);
$method = $class->getMethod("redirectTo");
$method->setAccessible(true);
$this->assertContains("",$method->invokeArgs($middleware,[$mock]));
}
I am using symfony 3 and trying to get access to the class I declared in
src/AppBundle/Service/ApiEngine.php
namespace AppBundle\Service;
use DateTime;
class ApiEngine {
private $api_handler;
public function __construct($username, bool $repos) {
$client = new \GuzzleHttp\Client();
$request = 'https://api.github.com/users/' . $username;
$request .= ($repos) ? '/repos' : "";
$res = $client->request('GET', $request);
$this->api_handler = json_decode($res->getBody()->getContents(), true);
}
public function getProfileData() {
return [ /*some data*/ ];
}
}
I declared this file in
config/service.yml
service:
*
*
*
api:
class: AppBundle\Service\ApiEngine
arguments: ["#username", "#repos"]
In controller I am trying to use some of the ApiEngine methods like this:
src/AppBundle/Controller/GitApiController.php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class GitApiController extends Controller {
/**
* #Route("/{username}", name ="gitapi", defaults={"username": "symfony"})
*/
public function gitApiAction($username) {
$api = $this->get('api')->__construct($username, false)->getProfileData();
return $this->render('gitapi/index.html.twig', $api);
} }
But it gives me this error:
(1/1) ServiceNotFoundException The service "api" has a dependency on a
non-existent service "username".
I advise you to change your class into this for example:
private function __construct($username, bool $repos) {
$client = new \GuzzleHttp\Client();
$request = 'https://api.github.com/users/' . $username;
$request .= ($repos) ? '/repos' : "";
$res = $client->request('GET', $request);
$this->api_handler = json_decode($res->getBody()->getContents(), true);
}
public static function createApiEngine($username, bool $repos)
{
return new self($username, $bool);
}
After inside your controller you can do this:
$api = ApiEngine::createApiEngine($username, false);
$api->getProfileData();
Into your controller you need to insert a use for ApiEngine, in this use case you don't need dependency injection so inside your services.yml remove arguments please