Warning: This question is Laravel 4 specific.
I've been using Facades in my controllers before. Therefore I know the code is working. Now I need to introduce dependency injection for various reasons.
After refactoring the controller I get following error:
Illuminate \ Container \ BindingResolutionException
Unresolvable dependency resolving [Parameter #0 [ $name ]].
I can't figure out where the problem is. The Error message seems cryptic to me and I don't understand it. (I don't see any problem with my __constructor parameters since I've registered the binding for the HelpersInterface)
Here are the important parts of my code:
File: app/start/global.php
<?php
// ...
App::bind('Acme\Interfaces\HelpersInterface', 'Acme\Services\Helpers');
File: composer.json
// ...
"autoload": {
// ...
"psr-0": {
"Acme": "app/"
}
},
// ...
File: app/Acme/Controllers/BaseController.php
<?php namespace Acme\Controllers;
use Carbon\Carbon;
use Controller;
use Illuminate\Foundation\Application as App;
use Illuminate\View\Factory as View;
use Acme\Interfaces\HelpersInterface as Helpers;
use Illuminate\Http\Response;
class BaseController extends Controller {
/**
* #var \Illuminate\Foundation\Application
*/
private $app;
/**
* #var \Carbon\Carbon
*/
private $carbon;
/**
* #var \Illuminate\View\Factory
*/
private $view;
/**
* #var \Acme\Interfaces\HelpersInterface
*/
private $helpers;
function __construct(App $app, Carbon $carbon, View $view, Helpers $helpers)
{
$this->app = $app;
$this->carbon = $carbon;
$this->view = $view;
$this->helpers = $helpers;
$lang = $this->app->getLocale();
$now = $this->carbon->now();
$this->view->share('lang', $lang);
$this->view->share('now', $now);
}
/**
* Missing Method
*
* Abort the app and return a 404 response
*
* #param array $parameters
* #return Response
*/
public function missingMethod($parameters = array())
{
return $this->helpers->force404();
}
}
File: app/Acme/Services/Helpers.php
<?php namespace Acme\Services;
use Illuminate\Config\Repository as Config;
use Illuminate\Database\Connection as DB;
use Illuminate\Http\Request;
use Illuminate\Routing\Redirector as Redirect;
use Illuminate\Session\Store as Session;
use Illuminate\Support\Facades\Response;
use Illuminate\Translation\Translator as Lang;
use Illuminate\View\Factory as View;
use Acme\Interfaces\MockablyInterface;
use Monolog\Logger as Log;
class Helpers implements HelpersInterface {
// ...
public function __construct(
Config $config,
Lang $lang,
View $view,
MockablyInterface $mockably,
Log $log,
Request $request,
Session $session,
DB $db,
Redirect $redirect,
Response $response
) {
// ...
}
// ...
}
File: app/Acme/Providers/HelpersServiceProvider.php
<?php namespace Acme\Providers;
use Illuminate\Support\ServiceProvider;
use Acme\Services\Helpers;
class HelpersServiceProvider extends ServiceProvider {
private $db;
private $defaultDbConnection;
protected function init()
{
$this->db = $this->app['db'];
$this->defaultDbConnection = $this->db->getDefaultConnection();
}
public function register()
{
$this->init();
$this->app->bind('helpers', function ()
{
return new Helpers(
$this->app['config'],
$this->app['translator'],
$this->app['view'],
$this->app['mockably'],
$this->app->make('log')->getMonolog(),
$this->app['request'],
$this->app['session.store'],
$this->db->connection($this->defaultDbConnection),
$this->app['redirect'],
$this->app['Illuminate\Support\Facades\Response']
);
});
}
For me it was just a matter of running
php artisan optimize:clear
It seems your Acme\Services\Helpers constructor takes a $name parameter, but is not type hinted.
Laravel's IoC is not magic. If your don't provide a type hint for every parameter, the IoC container has no way of knowing what to pass in.
Make sure you use Illuminate\Http\Request; on top of the file instead of any other http import like this
use Illuminate\Http\Request;
THANK ME LATER!
Got it fixed. All the tutorials about dependency injection were referring to concrete implementations of interfaces so that I thought that's the way to go about it. Joseph Silber's answer got me on the right track.
The trick is to bind the Interface to the binding of the ServiceProvider like shown below. That way Laravel will know how to instantiate the Helpers service.
File: app/start/global.php
<?php
// ...
App::bind('Acme\Interfaces\HelpersInterface', 'helpers');
Related
As my IDE points out, the AbstractController::getDoctrine() method is now deprecated.
I haven't found any reference for this deprecation neither in the official documentation nor in the Github changelog.
What is the new alternative or workaround for this shortcut?
As mentioned here:
Instead of using those shortcuts, inject the related services in the constructor or the controller methods.
You need to use dependency injection.
For a given controller, simply inject ManagerRegistry on the controller's constructor.
use Doctrine\Persistence\ManagerRegistry;
class SomeController {
public function __construct(private ManagerRegistry $doctrine) {}
public function someAction(Request $request) {
// access Doctrine
$this->doctrine;
}
}
You can use EntityManagerInterface $entityManager:
public function delete(Request $request, Test $test, EntityManagerInterface $entityManager): Response
{
if ($this->isCsrfTokenValid('delete'.$test->getId(), $request->request->get('_token'))) {
$entityManager->remove($test);
$entityManager->flush();
}
return $this->redirectToRoute('test_index', [], Response::HTTP_SEE_OTHER);
}
As per the answer of #yivi and as mentionned in the documentation, you can also follow the example below by injecting Doctrine\Persistence\ManagerRegistry directly in the method you want:
// src/Controller/ProductController.php
namespace App\Controller;
// ...
use App\Entity\Product;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\HttpFoundation\Response;
class ProductController extends AbstractController
{
/**
* #Route("/product", name="create_product")
*/
public function createProduct(ManagerRegistry $doctrine): Response
{
$entityManager = $doctrine->getManager();
$product = new Product();
$product->setName('Keyboard');
$product->setPrice(1999);
$product->setDescription('Ergonomic and stylish!');
// tell Doctrine you want to (eventually) save the Product (no queries yet)
$entityManager->persist($product);
// actually executes the queries (i.e. the INSERT query)
$entityManager->flush();
return new Response('Saved new product with id '.$product->getId());
}
}
Add code in controller, and not change logic the controller
<?php
//...
use Doctrine\Persistence\ManagerRegistry;
//...
class AlsoController extends AbstractController
{
public static function getSubscribedServices(): array
{
return array_merge(parent::getSubscribedServices(), [
'doctrine' => '?'.ManagerRegistry::class,
]);
}
protected function getDoctrine(): ManagerRegistry
{
if (!$this->container->has('doctrine')) {
throw new \LogicException('The DoctrineBundle is not registered in your application. Try running "composer require symfony/orm-pack".');
}
return $this->container->get('doctrine');
}
...
}
read more https://symfony.com/doc/current/service_container/service_subscribers_locators.html#including-services
In my case, relying on constructor- or method-based autowiring is not flexible enough.
I have a trait used by a number of Controllers that define their own autowiring. The trait provides a method that fetches some numbers from the database. I didn't want to tightly couple the trait's functionality with the controller's autowiring setup.
I created yet another trait that I can include anywhere I need to get access to Doctrine. The bonus part? It's still a legit autowiring approach:
<?php
namespace App\Controller;
use Doctrine\Persistence\ManagerRegistry;
use Doctrine\Persistence\ObjectManager;
use Symfony\Contracts\Service\Attribute\Required;
trait EntityManagerTrait
{
protected readonly ManagerRegistry $managerRegistry;
#[Required]
public function setManagerRegistry(ManagerRegistry $managerRegistry): void
{
// #phpstan-ignore-next-line PHPStan complains that the readonly property is assigned outside of the constructor.
$this->managerRegistry = $managerRegistry;
}
protected function getDoctrine(?string $name = null, ?string $forClass = null): ObjectManager
{
if ($forClass) {
return $this->managerRegistry->getManagerForClass($forClass);
}
return $this->managerRegistry->getManager($name);
}
}
and then
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use App\Entity\Foobar;
class SomeController extends AbstractController
{
use EntityManagerTrait
public function someAction()
{
$result = $this->getDoctrine()->getRepository(Foobar::class)->doSomething();
// ...
}
}
If you have multiple managers like I do, you can use the getDoctrine() arguments to fetch the right one too.
How can I mock a service in a functional test use-case where a "request"(form/submit) is being made. After I make the request all the changes and mocking I made to the container are lost.
I am using Symfony 4 or 5. The code posted here can be also found here: https://github.com/klodoma/symfony-demo
I have the following scenario:
SomeActions service is injected into the controller constructor
in the functional unit-tests I try to mock the SomeActions functions in order to check that they are executed(it sends an email or something similar)
I mock the service and overwrite it in the unit-tests:
$container->set('App\Model\SomeActions', $someActions);
Now in the tests I do a $client->submit($form); which I know that it terminates the kernel.
My question is: HOW can I inject my mocked $someActions in the container after $client->submit($form);
Below is a sample code I added to the symfony demo app
https://github.com/symfony/demo
in services.yaml
App\Model\SomeActions:
public: true
SomeController.php
<?php
namespace App\Controller;
use App\Model\SomeActions;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* Controller used to send some emails
*
* #Route("/some")
*/
class SomeController extends AbstractController
{
private $someActions;
public function __construct(SomeActions $someActions)
{
//just dump the injected class name
var_dump(get_class($someActions));
$this->someActions = $someActions;
}
/**
* #Route("/action", methods="GET|POST", name="some_action")
* #param Request $request
* #return Response
*/
public function someAction(Request $request): Response
{
$this->someActions->doSomething();
if ($request->get('send')) {
$this->someActions->sendEmail();
}
return $this->render('default/someAction.html.twig', [
]);
}
}
SomeActions
<?php
namespace App\Model;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
class SomeActions
{
private $mailer;
public function __construct(MailerInterface $mailer)
{
$this->mailer = $mailer;
}
public function doSomething()
{
echo 'doSomething';
}
public function sendEmail()
{
echo 'sendEmail';
$email = (new Email())
->from('hello#example.com')
->to('you#example.com')
->subject('Time for Symfony Mailer!')
->text('Sending emails is fun again!')
->html('<p>See Twig integration for better HTML integration!</p>');
$this->mailer->send($email);
}
}
SomeControllerTest.php
<?php
namespace App\Tests\Controller;
use App\Model\SomeActions;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class SomeControllerTest extends WebTestCase
{
public function testSomeAction()
{
$client = static::createClient();
// gets the special container that allows fetching private services
$container = self::$container;
$someActions = $this->getMockBuilder(SomeActions::class)
->disableOriginalConstructor()
->getMock();
//expect that sendEmail will be called
$someActions->expects($this->once())
->method('sendEmail');
//overwrite the default service: class: Mock_SomeActions_e68f817a
$container->set('App\Model\SomeActions', $someActions);
$crawler = $client->request('GET', '/en/some/action');
//submit the form
$form = $crawler->selectButton('submit')->form();
$client->submit($form);
//after submit the default class injected in the controller is "App\Model\SomeActions" and not the mocked service
$response = $client->getResponse();
$this->assertResponseIsSuccessful($response);
}
}
The solution is to disable the kernel reboot:
$client->disableReboot();
It makes sense if ones digs deep enough to understand what's going on under the hood;
I am still not sure if there isn't a more straight forward answer.
public function testSomeAction()
{
$client = static::createClient();
$client->disableReboot();
...
I don't understand Why I can't create custom services. I get errors with the both technic. I don't find anything about that in your doc.
# app/config/services.yml
services:
jdf.utils.phphelper:
class: JDF\Utils\PhpHelper
// src/JDF/Utils/PhpHelper.php
namespace JDF\Utils;
class PhpHelper
{
/**
* [pdebug description]
* #param string $var The string to beautiful show
* #param string $msg Description of the $var
* #param integer $displayNone
* #return echo pre print_r $var string
*/
public function pdebug ($var, $msg = '', $displayNone = 0) {
}
}
Case 1 : (Pass PhpHelper in the __construct function)
// src/JDF/CsvTreatmentBundle\Controller/ImportController
namespace JDF\CsvTreatmentBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
use JDF\Utils\PhpHelper;
use Psr\Log\LoggerInterface;
/**
*
*/
class ImportController extends Controller {
function __construct(
PhpHelper $PhpHelper
) {
}
public function indexAction() {
//$test = $this->container->get('jdf.utils.phphelper');
return new Response('<hr>');
}
} /*End of class*/
Error 1 :
Catchable Fatal Error: Argument 1 passed to JDF\CsvTreatmentBundle\Controller\ImportController::__construct() must be an instance of JDF\Utils\PhpHelper, none given, called in C:\kitutilitaire\vendor\symfony\symfony\src\Symfony\Component\HttpKernel\Controller\ControllerResolver.php on line 202 and defined
500 Internal Server Error - ContextErrorException
Case 2 (just use get() controller method) :
// src/JDF/CsvTreatmentBundle\Controller/ImportController
namespace JDF\CsvTreatmentBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
use JDF\Utils\PhpHelper;
use Psr\Log\LoggerInterface;
/**
*
*/
class ImportController extends Controller {
function __construct(
//PhpHelper $PhpHelper
// LoggerInterface $logger
) {
}
public function indexAction() {
$test = $this->container->get('jdf.utils.phphelper');
// $logger = $this->container->get('logger');
return new Response('<hr>');
}
} /*End of class*/
Error 2 :
Attempted to load class "PhpHelper" from namespace "JDF\Utils".
Did you forget a "use" statement for another namespace?
Stack Trace
in var\cache\dev\appDevDebugProjectContainer.php at line 3555 -
*/
protected function getJdf_Utils_PhphelperService()
{
return $this->services['jdf.utils.phphelper'] = new \JDF\Utils\PhpHelper();
}
/**
EDIT : composer.json autoload
"autoload": {
"psr-4": {
"AppBundle\\": "src/AppBundle/",
"JDF\\CsvTreatmentBundle\\": "src/JDF/CsvTreatmentBundle/",
"JDF\\Utils\\": "src/JDF/Utils/PhpHelper"
},
"classmap": ["app/AppKernel.php", "app/AppCache.php"]
},
Thank in advence for your help.
Controllers do not get any injection by default. They have $this->container always available to get to all your services.
So nothing more to do than:
class ImportController extends Controller {
public function indexAction() {
$test = $this->container->get('jdf.utils.phphelper');
// $logger = $this->container->get('logger');
return new Response('<hr>');
}
}
FYI: The cache file appDevDebugProjectContainer is auto generated and of no significance to your problem.
I've solved the problem with change my composer.json.
For can use $this->container->get('jdf.utils.phphelper'); the all good code is :
# app/config/services.yml
services:
jdf.utils.phphelper:
class: JDF\Utils\PhpHelper
// src/JDF/Utils/PhpHelper.php
namespace JDF\Utils;
class PhpHelper {}
// src/JDF/CsvTreatmentBundle\Controller/ImportController
namespace JDF\CsvTreatmentBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use JDF\Utils\PhpHelper;
/**
*
*/
class ImportController extends Controller {
public function indexAction() {
$test = $this->container->get('jdf.utils.phphelper');
return new Response('<hr>');
}
} /*End of class*/
AND IMPORTANT : The composer.json :
"autoload": {
"psr-4": {
"JDF\\CsvTreatmentBundle\\": "src/JDF/CsvTreatmentBundle/",
"JDF\\Utils\\": "src/JDF/Utils/"
},
"classmap": ["app/AppKernel.php", "app/AppCache.php"]
},
And the CLI command : php composer.phar dump-autoload
Thank to colburton for this time and interest at my issue.
Since the last 4 hours I'm trying to understand the logic of Symfony 2 services and how they integrate in the application...
Basically I'm trying to set my EntityManager via a service and use it in a controller
I have the following structure
Bundle1/Controller/Bundle1Controller.php
Bundle1/Services/EntityService.php
Bundle2/Controller/Bundle2Controller.php
Bundle3/Controller/Bundle3Controller.php
....
I'm trying to make a REST API with different entry points, that's why I use multiple bundles bundle2,bundle3....
The logic is the following:
A POST is fired to Bundle2/Controller/Bundle2Controller.php
Bundle2Controller.php instances a new() Bundle1Controller.php
Inside Bundle1Controller I want to access a service entity_service in order to get my EntityManager
I have 2 cases in which I manage to land...
In Bundle1/Controller/Bundle1Controller if I try $this->container or $this->get('entity_service') I get a null everytime
If I set the container in Bundle2/Controller/Bundle2Controller and try $this->get('entity_service') I get You have requested a non-existent service "entity_service"
I will place all the code below
Bundle1/Controller/Bundle1Controller
<?php
namespace Bundle1\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use EntityBundle\Entity\TestEntity;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
class Bundle1Controller extends Controller
{
/**
* #param $response
* #return array
*/
public function verifyWebHookRespone($response){
$em = $this->get('entity_service')->getEm();
$array = json_decode($response);
$mapping = $em->getRepository('EntityBundle:TestEntity')
->findBy(["phone" => $array['usernumber']]);
return $mapping;
}
}
Bundle2/Controller/Bundle2Controller.php
<?php
namespace Bundle2\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Bundle1\Controller\Bundle1Controller;
class Bundle2Controller extends Controller
{
public function webhookAction(Request $request)
{
$data = $request->request->get('messages');
$model = new Bundle1Controller();
$responseMessage = $model->verifyWebHookRespone($data);
return new Response($responseMessage, Response::HTTP_CREATED, ['X-My-Header' => 'My Value']);
}
}
Bundle1/Services/EntityService.php
<?php
namespace EntityBundle\Services;
use Doctrine\ORM\EntityManager;
use Symfony\Component\DependencyInjection\Container;
class EntityService
{
protected $em;
private $container;
public function __construct(EntityManager $entityManager, Container $container)
{
$this->em = $entityManager;
$this->container = $container;
}
/**
* #return EntityManager
*/
public function getEm()
{
return $this->em;
}
}
services.yml
services:
entity_service:
class: Bundle1\Services\EntityService
arguments: [ "#doctrine.orm.entity_manager" , "#service_container" ]
Can anyone please help me with something regarding this issue?
How can I register a service and call it from anywhere no matter the bundle or another service?
You should check where your services.yml is located and whether it is imported in the config.yml
You can't just instantiate a controller and expect it to work, you need to set the container.
But you can call EntityManager without needing any other service by using;
$this->get('doctrine.orm.entity_manager');
I can't understand your structure or what you are trying to achieve, but those are the options to go about if you want to keep this structure.
I would like to know if there is a magic method to use this scenario :
If I call a page via an AJAX request the controller returns a JSON object, otherwise it returns a view, i'm trying to do this on all my controllers without changin each method.
for example i know that i can do this :
if (Request::ajax()) return compact($object1, $object2);
else return view('template', compact($object, $object2));
but I have a lot of controllers/methods, and I prefer to change the basic behavior instead of spending my time to change all of them. any Idea ?
The easiest way would be to make a method that is shared between all of your controllers.
Example:
This is your controller class that all other controllers extend:
<?php namespace App\Http\Controllers;
use Illuminate\Routing\Controller as BaseController;
abstract class Controller extends BaseController
{
protected function makeResponse($template, $objects = [])
{
if (\Request::ajax()) {
return json_encode($objects);
}
return view($template, $objects);
}
}
And this is one of the controllers extending it:
<?php namespace App\Http\Controllers;
class MyController extends Controller
{
public function index()
{
$object = new Object1;
$object2 = new Object2;
return $this->makeResponse($template, compact($object, $object2));
}
}
Update for Laravel 5+
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
class Controller extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
protected function makeResponse($request, $template, $data = [])
{
if ($request->ajax()) {
return response()->json($data);
}
return view($template, $data);
}
}
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class MyController extends Controller
{
public function index(Request $request)
{
$object = new Object1;
$object2 = new Object2;
return $this->makeResponse($request, $template, compact($object, $object2));
}
}
There is no magic but you can easily override ViewService in 3 steps:
1.create your view factory (your_project_path/app/MyViewFactory.php)
<?php
/**
* Created by PhpStorm.
* User: panos
* Date: 5/2/15
* Time: 1:35 AM
*/
namespace App;
use Illuminate\View\Factory;
class MyViewFactory extends Factory {
public function make($view, $data = array(), $mergeData = array())
{
if (\Request::ajax()) {
return $data;
}
return parent::make($view, $data, $mergeData);
}
}
2.create your view service provider (your_project_path/app/providers/MyViewProvider.php)
<?php namespace App\Providers;
use App\MyViewFactory;
use Illuminate\View\ViewServiceProvider;
class MyViewProvider extends ViewServiceProvider {
/**
* Register the application services.
*
* #return void
*/
public function register()
{
parent::register();
}
/**
* Overwrite original so we can register MyViewFactory
*
* #return void
*/
public function registerFactory()
{
$this->app->singleton('view', function($app)
{
// Next we need to grab the engine resolver instance that will be used by the
// environment. The resolver will be used by an environment to get each of
// the various engine implementations such as plain PHP or Blade engine.
$resolver = $app['view.engine.resolver'];
$finder = $app['view.finder'];
// IMPORTANT in next line you should use your ViewFactory
$env = new MyViewFactory($resolver, $finder, $app['events']);
// We will also set the container instance on this view environment since the
// view composers may be classes registered in the container, which allows
// for great testable, flexible composers for the application developer.
$env->setContainer($app);
$env->share('app', $app);
return $env;
});
}
}
3.in your_project_path/config/app.php:
change 'Illuminate\View\ViewServiceProvider',
to 'App\Providers\MyViewProvider',
What this do:
it tells your application to use another view provider which will register your view factory
$env = new MyViewFactory($resolver, $finder, $app['events']);
in line 33 of MyViewProvider.php which will check if request is AJAX and return if true or continue with original behavior
return parent::make($view, $data, $mergeData);
in MyViewFactory.php line 19
Hope this help you,
In laravel 5.1, this is the best way:
if (\Illuminate\Support\Facades\Request::ajax())
return response()->json(compact($object1, $object2));
else
return view('template', compact($object, $object2));
The solution suggested by #ryanwinchester is really good. I, however, wanted to use it for the responses from update() and delete(), and there naturally return view() at the end doesn't make a lot of sense as you mostly want to use return redirect()->route('whatever.your.route.is'). I thus came up with that idea:
// App\Controller.php
/**
* Checks whether request is ajax or not and returns accordingly
*
* #param array $data
* #return mixed
*/
protected function forAjax($data = [])
{
if (request()->ajax()) {
return response()->json($data);
}
return false;
}
// any other controller, e.g. PostController.php
public function destroy(Post $post)
{
// all stuff that you need until delete, e.g. permission check
$comment->delete();
$r = ['success' => 'Wohoo! You deleted that post!']; // if necessary
// checks whether AJAX response is required and if not returns a redirect
return $this->forAjax($r) ?: redirect()->route('...')->with($r);
}