3

Original Question

I've read every page of the "book" about service containers, and I'm still baffled because things seem to randomly not work nearly every time I try to use $this->container. For example, I'm building a form in my custom bundle controller following the instructions.

My controller extends the base controller as usual:

namespace Gutensite\ArticleBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Gutensite\ArticleBundle\Entity\Article;

class AdminEditController extends Controller
{

    public function indexAction() {


        $content = new Article();
        $form = $this->createFormBuilder($content)
             ->add('content', 'text');

        // same issue with the shortcut to the service which I created according the instructions
        // $form = $this->createForm('myForm', $myEntity)

        //...more code below...
    }
}

This produces an error:

Fatal error: Call to a member function get() on a non-object in /vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php on line 176

If we look at that file at that line number we see Symfony's code:

public function createFormBuilder($data = null, array $options = array())
{
    return $this->container->get('form.factory')->createBuilder('form', $data, $options);
}

So WHY is symfony's own controller NOT able to access the container->get() function?!

What am I doing wrong?

Along these same lines, I can't figure out why sometimes I can't access the container via $this->container in my own controller (if extend the framework controller or if I reference it by passing it in the construct, etc). It seems random...


Background of Project and Structure of Code

I am building a CMS that has user's routes (URLs) stored in a database. So I have one route defined which directs all requests to my main CMS Controller:

gutensite_cms_furl:
# Match Multiple Paths (the plain / path appears necessary)
path:     /
path:     /{furl}
defaults: { _controller: GutensiteCmsBundle:Init:index }
# Allow / in friendly urls, through more permissive regex
requirements:
    furl: .*

The InitController looks up the requested URL and gets the correct Route entity which points to a View entity that defines which Bundle and Controller to load for specific page type being requested, e.g. the route for /Admin/Article/Edit points to content type that is associated with the Article bundle and AdminEdit controller, which then creates a new object for this content type (Gutensite\ArticleBundle\Controller\AdminEditController.php) and executes the required functions. This then injects the necessary variables back into the main ViewController which gets passed to the template to be rendered out to the page.

This main controller extends symfony controller and I have confirmed that the container is accessible in this controller, e.g. $this->container->get('doctrine') works.

// Gutensite\CmsBundle\Controller\InitController.php
namespace Gutensite\CmsBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Gutensite\CmsBundle\Entity;

class InitController extends Controller
{
    public function indexAction(Request $request, $furl)
    {
        // Confirm container is accessible (yes it is)
        $test = $this->container->get('doctrine');

        // Look up the View Entity based on the Route Friendly URL: $furl
        $viewController = $this->container->get('gutensite_cms.view');
        $viewController->findView($furl, $siteId);

        // Load the Requested Bundle and Controller for this View
    $path = $viewController->view->namespace_controller."\\".$viewController->view->controller;
    $content = new $path;
        // Execute the main function for this content type controller, which adds variables back into the $viewController to be passed to the template.
    $content->indexAction($viewController);

        return $this->render(
        $viewController->view->bundle_shortcut.'::'.$viewController->view->getTemplatesLayout(),
            array('view' => $viewController)
    );


    }
}

FYI, the ViewController is defined as a global service:

services:
    gutensite_cms.view:
        class: Gutensite\CmsBundle\Controller\ViewController
        arguments: [ "@service_container" ]

And then Below is a simplified version of the Gutensite/CmsBundle/Controller/ViewController.php

namespace Gutensite\CmsBundle\Controller;
use Doctrine\ORM\EntityManager;
use Symfony\Component\DependencyInjection\ContainerInterface as Container;

class ViewController
{

    protected $container;
    public $routing;
    public $view;

    public function __construct(Container $container) {
        $this->container = $container;
    }

    public function findView($furl, $siteId=NULL) {
        $em = $this->container->get('doctrine')->getManager();
        $this->routing = $em->getRepository('GutensiteCmsBundle:Routing\Routing')->findOneBy(
            array('furl'=>$furl, 'siteId'=>$siteId)
        );

        if(empty($this->routing)) return false;

        // If any redirects are set, don't bother getting view
        if(!empty($this->routing->getRedirect())) return FALSE;

        // If there is not view associated with route
        if(empty($this->routing->getView())) return FALSE;

        $this->view = $this->routing->getView();
        $this->setDefaults();
    }
}

Back in the InitController.php we retrieved the view object and loaded the right bundle and controller function. In this case it loaded `Gutensite\ArticleBundle\Controller\AdminEditController.php which is where we lose access to the service container.

namespace Gutensite\ArticleBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Gutensite\ArticleBundle\Entity\Article;

class AdminEditController extends Controller
{

    protected $request;

    public function __contstruct(Request $request) {
        $this->request = $request;
}


public function indexAction($view)
    {
    // TEST: Test if I have access to container (I do not)
    //$doctrine = $this->container->get('doctrine');

        // This loads createForm() function from the Symfony Controller, but that controller then doesn't have access to container either.
        $form = $this->createForm('view', $content);

    }
}

More Specific Question

So I ASSUMED that if you extend the Symfony Controller, which itself extends ContainerAware, that the object would be "aware of the container". But that evidently is not the case. And that is what I need to understand better. I assume somehow the container has to be injected manually, but why? And is that the standard method?

7
  • Can you show the route information for this controller? Commented Apr 23, 2014 at 6:07
  • Because your route is specifying the controller using a service id and you are not injecting the container into the controller. Hence the request to see your routing information. Just need to make a simple adjustment to your services.yml file. Commented Apr 23, 2014 at 13:07
  • @WouterJ I am rebuilding 2.0 of our CMS using Symfony, and so it is necessary that the routes be defined dynamically in a database. I have ONE route, that loads my Gutensite\CmsBundle\Controller\InitController.php which extends the framework Controller. It then looks in a Routes table which can have one or more URLs that ultimately point to a view object that defines the appropriate bundle and controller to load for the page type being requested. This works. And from within my controllers I am able to access controller functions like $this->createFormBuilder() so the container is accessible. Commented Apr 23, 2014 at 16:06
  • @Cerad You said my "route is specifying the controller using a service id"? Where are you deducing that from and what does it mean? My controller extends the Framework Controller, so why do I need to inject the container into it? Isn't that one of the main points of extending the Symfony Controller, which itself extended ContainerAware? So as I mentioned, from within MY controller, the container appears to be accessible, but within the symfony controller, the container is not... Commented Apr 23, 2014 at 16:11
  • I deduced based on the symptoms. You are correct that by default, extending from the base symfony controller is all you need to do. Commented Apr 23, 2014 at 16:32

1 Answer 1

2

Ok. Your assumption that merely making an object ContainerAware will automatically cause the container to be injected is incorrect. The PHP new operator does not know anything about dependencies. It's the job of the dependency injection container to take care of automatically injecting stuff. And of course your are not using the container to create your controllers.

Easy enough to fix:

$path = $viewController->view->namespace_controller."\\".$viewController->view->controller;
$content = new $path;
$content->setContainer($this->container);
$content->indexAction($request,$viewController);

I don't really follow your flow. The view stuff seems kind of backwards to me but I trust you can see where and how the container is injected into a Symfony controller. Don't do anything in the controller's constructor which relies on the container.

===============================================================

Instead of using the new operator, you could use the service container.

$contentServiceId = $viewController->view->contentServiceId;
$content = $this->container->get($contentServiceId);
$content->indexAction($request,$viewController);

Instead of having you view return a class name, have it return a service id. You then configure your controller in services.yml and off you go. This cookbook entry might help a bit: http://symfony.com/doc/current/cookbook/controller/service.html

=============================================================

All ContainerAware does is to make the Symfony DependencyInjectContainer inject the container. Nothing more. Nothing less. You might conside reading through here: http://symfony.com/doc/current/components/dependency_injection/index.html just to get basic idea of what dependency injection and dependency injector container are all about.

Sign up to request clarification or add additional context in comments.

8 Comments

I'd also LOVE feedback about how to better structure my "flow", if you see better ways of doing this. I am using this project as a learning opportunity, and will inevitably refactor everything many times by the time I learn Symfony properly.
I tried creating setContainer() in my $content class but that gave an error and now I see that setContainer() is an existing function in the extended Symfony Controller. So this must be a common practice. That did successfully inject the container. Bravo (clap).
Basically yes. You might take a look at Symfony\Componen\HttpKernel\HttpKernel::handleRaw to get a better understanding of the default Symfony flow. A controller resolver class takes care of injecting the container.
In the end, you should probably replace your InitController with a kernel request listener. The listener will take care of looking up $furl in the database and setting the $request::_controller property. At that point, the default Symfony implementation can take over. But you will need to take the time to understand how Symfony processes a request into a response for this to make sense.
Probably not. Eventually much of what you describe could go in one or more listeners. But as long as you are making progress then I'd continue down your current path.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.