Take for example the following controller/action:
public function indexAction()
{
return $this->render('TestBundle:TestController:index.html.twig');
}
I would like to write the template expression (or whatever it's name is) this way:
public function indexAction()
{
return $this->render('*:TestController:index.html.twig');
}
So that symfony knows I'm looking for a template in this very bundle. Having to write the whole Owner + Bundle for every template/action/repository I want to refer is very annoying. Even more so considering most of the time I refer to actions and templates in the same bundle.
NOTE: I know templates can be put at the app level and be refernced like this:
'::index.html.twig'
But that is not what I need.
It's possible with a bit of custom code.
Basically, you want to override the controller's render() method and include logic to fetch the name of the current bundle.
Note that instead of my controllers extending Symfony\Bundle\FrameworkBundle\Controller\Controller, they extend a custom controller (which then extends Symfony's controller). This allows you to conveniently give the controller more ability by adding your own methods.
Ex:
MyBundle\Controller\MyController\ extends MyCustomBaseController which extends Symfony\Bundle\FrameworkBundle\Controller\Controller.
So, in my custom controller I have these two methods:
public function render($view, array $parameters = array(), Response $response = null) {
$currentBundle = $this->getCurrentBundle();
$view = str_replace('*', $currentBundle, $view);
return parent::render($view, $parameters, $response);
}
public function getCurrentBundle() {
$controller = $this->getRequest()->attributes->get('_controller');
$splitController = explode('\\', $controller);
return $splitController[1];
}
Take a look at render(). It fetches the current bundle name and uses it to build the $view variable. Then it just calls parent::render() and it's as if you had manually defined the bundle name in the render statement.
The code here is very simple, so you should be able to easily extend it to do other things, such as allow you to also avoid typing the controller name.
Important: If you do use a custom controller, make sure you use Symfony\Component\HttpFoundation\Response, otherwise PHP will complain that the method signatures for render() don't match.
Related
I have been declaring all the routes for my application inside web.php , but it is now getting quite large. I find that I am losing a lot of time shifting between web.php and each controller and this is hurting productivity.
I feel like it would be better to define routes inside of the controller, perhaps ideally delegating some URL to a controller and then allowing the controller to handle the "sub routes" since this would allow me to use inheritance when I have two similar controllers with similar routes.
It is not possible given how laravel works. Every request is passed onto router to find its designated spot viz. the controller with the method. If it fails to find the route within the router, it just throws the exception. So the request never reaches any controller if the route is not found. It was possible in earlier versions on Symphony where you would configure the route in the comment of a particular controller method.
Sadly with laravel it works how it works.
But for me, I just like to have the routes in a separate file.
Alternate solution, easier way to sort all the routes.
You can move your route registration into controllers if you use static methods for this. The code below is checked in Laravel 7
In web.php
use App\Http\Controllers\MyController;
.....
MyController::registerRoutes('myprefix');
In MyController.php
(I use here additional static methods from the ancestor controller also posted below)
use Illuminate\Support\Facades\Route;
.....
class MyController extends Controller {
......
static public function registerRoutes($prefix)
{
Route::group(['prefix' => $prefix], function () {
Route::any("/foo/{$id}", self::selfRouteName("fooAction"));
Route::resource($prefix, self::selfQualifiedPath());
}
public function fooAction($id)
{
........
}
In Controller.php
class Controller extends BaseController {
....
protected static function selfAction($actionName, $parameters = [], $absolute = false)
{
return action([static::class, $actionName], $parameters, $absolute);
}
protected static function selfQualifiedPath()
{
return "\\".static::class;
}
protected static function selfRouteName($actionName)
{
//classic string syntax return "\\".static::class."#".$actionName;
// using tuple syntax for clarity
return [static::class, $actionName];
}
}
selfAction mentioned here is not related to your question, but mentioned just because it allows making correct urls for actions either by controller itself or any class using it. This approach helps making action-related activity closer to the controller and avoiding manual url-making. I even prefer making specific functions per action, so for example for fooAction
static public function fooActionUrl($id)
{
return self::selfAction('foo', ['id' => $id]);
}
Passing prefix into registerRoutes makes controller even portable in a sense, so allows inserting it into another site with a different prefix in case of conflict
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?
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.
I'm little bit confused. I want to build my own framework just to learn how everything works not that I will use it for big projects.
I have a FrontController class and this class has the route functionality inside.
Functions to set/get Parameters for Controller
Functions to set/get Actions (methods) from Controller
Functions to parse the requested URI and return proper controller if exists if not returns default controller which is IndexController.
Run() method that does the following:
public function run() {
$method = new \ReflectionMethod($this->controller, $this->action);
$numParams = $method->getNumberOfParameters();
//fill missing parameters with null values
if (count($this->params) != $numParams) {
$tempArray = array_fill(0, $numParams, null);
$this->setParams($this->params + $tempArray);
}
$controller = new $this->controller;
$userInstance = User::getInstance();
//just creates a model based on the controller name by default its
//Index.php (model)
$model = DB::createModel($this->getControllerName());
//run _before before any function
$controller->_before($model, $userInstance);
call_user_func_array(array($controller, $this->action), $this->params);
return;
}
now I've seen tutorials and they use BaseController and each Controller then extends from this basecontroller. My controllers do not extend from FrontController.
My question is Do i need a separate class for Routing? DO i need to split FrontController into
BaseController
Route.php
Model.php
Since run() function actually passes the model and user object to the controller.
One basic principle to keep in mind is the Single Responsibility Principle. A well designed class has exactly one responsibility.
So yes. You will need to separate the routing and all other responsibilities.
Also note that the model must be considered a layer and not a class or object. The model layer is a collection of classes (data access, services). In fact, your User class should be considered part of that layer.
Here's an article which can help you understand the MVC pattern a bit better.
I'm little bit new to Zend, I want to use a controller action through out the entire application automatically, I don't have a clear idea to how to use it, thought about init() method, action helpers, etc.
Then instead of simply creating controller action create controllerAction Helper . Here you can find more about it
http://framework.zend.com/manual/en/zend.controller.actionhelpers.html
My_Helper_Magic extends Zend_Controller_Action_Helper_Abstract
{
public function preDispach()
{
//code inside here will run for entire application automatically
}
}
In your bootstrap do
Zend_Controller_Action_HelperBroker::addHelper(new My_Helper_Magic());
In Response to comment .
It depends upon your 'code fragment' , If your code fragment does not required to know nothing about module , controller, action , base url then you can use Bootstrap init function
like
public function _initAlways()
{
//see how this function name is prefixed with _init hence it will be called by ZF //everytime. You can put your code fragment here
//If your code fragment depends upon some stuff like baseurl then do action controller
// registration here instead
Zend_Controller_Action_HelperBroker::addHelper(new My_Helper_Magic());
}
In Response To comment
You can save any instance of your object inside Zend_Registy and retrieve it whereever you like
Inside Bootstrap.php
public function _initSetup()
{
$object = new My_Custom_Object();
Zend_Registry::set('my_custom_object',$object);
}
Later in your view or controller do
$myObject = Zend_Registry::get('my_custom_object'); //to access it
I am working on building a lightweight MVC, mainly for the learning process but I would like it to be good enough to use eventually.
Below is a basic example/demo of how a basic controller might would look, let's assume the URI has been processed and routed to this controller and these 2 methods.
1) I need to get data from database/cache/etc... inside my Model classes, I just need help on how I should load my models into my example controller below, you can see that I have added this below $profileData = $this->model->getProfile($userId) that is just made up and does not exist's, how could I get something like that to work though? Or should I load the model into the class a different way?
2) A lot of pages will require a user to be logged into the site. SHould I process that part below in the controller to check if a user is logged in, example, before building the profile page, check if user is logged in, if not then build a login page instead and add these checks inside of each controller method/page?
/**
* Example Controller
*/
class User_Controller extends Core_Controller {
// domain.com/user/id-53463463
function profile($userId)
{
//GET data from a Model
$profileData = $this->model->getProfile($userId);
$this->view->load('userProfile', $profileData);
}
// domain.com/user/friends/
function friends()
{
//GET data from a Model
$friendsData = $this->model->getFriendlist();
$this->view->load('userFriends', $friendsData);
}
}
core
abstract class Core_Controller {
protected $view;
protected $model;
function __construct(DependencyContainer $dependencyContainer){
$this->view = new Core_View();
//$this->view = $dependencyContainer->get(view);
}
}
There are probably tons of ways to accomplish what you are trying.
The "easiest" is probably to just override the constructor and instantiate the model directly.
in User_Controller:
public function __construct(DependencyContainer $dc) {
parent::__construct($dc);
$this->model = new User_Model();
}
I'm guessing that you are looking for something a little more automated though. If you want the Model to have the same name as the controller minus "_Controller", just use get_class($this) in the constructor and use PHP's string functions to parse out what you want. Once you have that in a variable, you can use that variable to instantiate the model:
in Core_Controller:
public function __construct(DependencyContainer $dc) {
$this->view = new Core_View();
// $model_class should be 'User_Model' now
$model_class = str_replace('_Controller', '_Model', get_class($this));
// now instantiate the model
$this->model = new $model_class();
}
I haven't actually worked with any framework that can only have one model associated with each controller (except may CakePHP? I can't remember). With Symfony, the models and controllers are completely decoupled so you can use any model with any controller. You just instantiate the model as need. Symfony use the Doctrine ORM so for example, in a controller action, if you needed a model you would do something like this:
$model = Doctrine::getTable('User');
It might be worthwhile to consider a design more like that in order to promote a decoupled design and I promise that you will want more than one model in some controller at some point.
2.) As far as authentication. Something that seems to be fairly common is to have some sort of setting (whether in a config file or a member variable) that says whether or not the current action needs the user to be authenticated. This is processed each time the action runs (Yii calls these kinds of things filters). If the user needs to be logged in, it stores the page that they are trying to access, and then redirects them to a log in page (you should only ever have to create one). Once they properly authenticate, it will redirect them back to where they were originally heading.