At the current state of 4.0.0-alpha4 the controller for a SiteTree model must live in the same namespace as the model itself. This leads to controllers that are namespaced like Batman\Batcave\Model\MyPageController, where it might be more relevant to put it into the Batman\Batcave\Controllers namespace.
Via Slack it was briefly discussed, and an option is to add a configuration static to SiteTree which can define the controller name.
Options:
$frontend_controller) would be null by default__construct would define it as the current behaviour, which is the FQ class name with Controller on the end (and _Controller) for deprecated supportgetControllerName can be moved to SiteTree::__construct in this caseConsiderations:
$frontend_controller property - it was defined but could not be found!"I generally think this is a good idea but I think that extra logic in SiteTree::__construct() is a really bad idea: it's called literally hundreds of times on every pageview.
Instead, I think that a new method, SiteTree::getFrontendController() should implement both the config lookup and the legacy functionality, and return a controller instance.
And how about the considerations around a member property vs a config static?
use a config static i think, and pass its value to Injector::inst()->get($value) to get the controller, rather than using new
Sorry, this should have been logged on the CMS module.
From a purist's point of view shouldn't Controllers nominate the Model they handle and not the other way round? Controllers act on Models and so need knowledge about the Model but the Model doesn't act on a control and should be agnostic to what Controller(s) can "handle" it?
With a Config value don't we have a problem where every Model now needs this config? How about we have a SiteTree (or Page) config which is the controller namespace rather than the FQNS for the Controller class?
Edit: Maybe the namespace config won't work elegantly either modules extend Page and will have a different namespace
From a purist's point of view shouldn't Controllers nominate the Model they handle and not the other way round? Controllers act on Models and so need knowledge about the Model but the Model doesn't act on a control and should be agnostic to what Controller(s) can "handle" it?
That's a good point. Strictly the ModelAsController would need a family of configuration that let is dispatch to the right controller based on the model that it looked up.
If this was done on an instance basis you'd be able to have two parallel websites based on the same SiteTree database, which would be interesting. It might lead to a better way of implementing things like multi-lingual sites and/or draft views.
With a Config value don't we have a problem where every Model now needs this config? How about we have a SiteTree (or Page) config which is the controller namespace rather than the FQNS for the Controller class?
You've already identified that simple rules could get too magical. However, another way to approach this would be to define a dispatcher interface that was tied into ModelAsController as a dependency:
class DataObjectControllerDispatch {
function controllerFor(DataObject $model);
}
Then you could have a few different implementations:
Controller or _Controller (a configurable array of possible suffixes would work)To work with modules and the like you'd probably need to have a cascade of such handlers, which return null if they couldn't find a match.
Sorry, this is the issue - reopening!
Do we want to allow @robbieaverill to get away with the most basic option (the original suggestion) which provides a resolution now and then we (cough @robbieaverill) can look into the alternatives?
I think that something should be decided on and put in before beta, but I don't see a whole lot of merit in doing it in stages to be honest
OK :)
Well, I'd always like to follow the purist's path... ;)
I like the idea of having a "dispatch" model for defining the relationship for a controller to a DataObject.
In a way I agree with @dhensby that it shouldn't be the job of the DataObject to define its controller, but at the same time I also don't think it should be the job of a controller to define the DataObject that it controls (unless you can define multiple), because it then limits a controller to 'controlling' a single DataObject.
I'm just trying to think... we want the configuration for this to either have a default, or be easy to configure... but we also want it to work so that if you have MyPage that extends Page, you don't necessarily need a controller for MyPage, and it would fall back to the controller for Page if not defined.
We potentially get into an area where the traditional one-to-one DO-to-controller becomes many-many, because a DataObject could be controlled by multiple controllers, and a controller could control multiple DataObjects 😄
We also need DataObject to be able to return its controller, so the inverse relationship needs to be easy enough to look up from wherever it is defined. If we define Controller: controls: MyDataObject in config then we have problems looking it up quickly because MyDataObject is value, not a key.
I'm not really producing anything worthwhile here, but basically I think for the sake of performance, we need to have a relationship from the DataObject to the Controller (in that direction) in the way it currently behaves. I'm 100% open to ideas about how to do the inverse efficiently though!!
I'm just trying to think... we want the configuration for this to either have a default, or be easy to configure... but we also want it to work so that if you have MyPage that extends Page, you don't necessarily need a controller for MyPage, and it would fall back to the controller for Page if not defined.
I think we could achieve that with Sam’s proposal. It’d just be a case of shifting the logic from getControllerName() into a dispatcher. As he also suggested, we could have “A remapping of model-namespace to controller-namespace with an optional suffix value”. That may be too expensive a look-up to go through on each page load, but you get the idea - there’s enough flexibility there to cater for most. What we’d provide by default I’m not sure!
We also need
DataObjectto be able to return its controller
I think we can do away with this - SiteTree::getControllerName() is the only way we do this in core (I think?), and as Dan said it’s arguably an anti-pattern anyway. We could still have a convenience method ::controller_for() somewhere - it’d just look through the dispatchers instead. For the VirtualPage issue this code originally targeted, we’d need to push an extra dispatcher to the front of the queue to handle VirtualPage specifically.
- Do we want to aim for a many-many model to controller relationship for the future of SilverStripe, a one-to-one, one-to-many (model to controller) or many-to-one (model to controller)?
My assumption is that it’d _effectively_ be many-to-one (model to controller), as the dispatcher lookup would stop as soon as it found the first suitable controller - even if many others are available for that model. There’d be nothing to stop you manually constructing a different controller instance for a model though.
I think Controllers can/should only handle one type of Model - that's how it is at the moment anyway. At least where "type" is a parent class of the model that can be handled.
Page_Controller can only handle Page model (or any descendants of it).
A BlogListingPage_Controller can't handle a HomePage model because it won't conform to the correct API or have the right relationships.
I think a controller should be able to say it handles one object and that's about it. The default dispatcher will prioritise controllers in mysite first and then find the first defined controller from modules.
Regarding performant lookup by Model name, this can be cached as part of the manifest and accessed through ClassInfo.
@dhensby that sounds good to me.
I think that the idea that things should be bound based on the class that they belong to is "static thinking" and so generally should be avoided.
A dispatcher (or content-controller factory) is able to provide either a model-specific controller, or a controller designed to work with multiple models (e.g. any model that has a particular extension attached), depending on its implementation.
Interface:
namespace SilverStripe\CMS\Controllers;
interface ModelAsControllerDispatch {
/**
* Returns the controller used to handle the given model
* @return Controller
* @throws OutOfBoundsException If the given model isn't handled by this controller
*/
function controllerFor(DataObject $model);
/**
* Returns true if this dispatcher is able to provide a controller for the given model
* @return boolean
*/
function hasControllerFor(DataObject $model);
}
Class:
/**
* Default mode-as-controller behaviour - suffixes the class with "_Controller" or "Controller"
*/
class DefaultModelAsControllerDispatch implements ModelAsControllerDispatch { ... }
Built-in yaml:
SilverStripe\CMS\Controllers\ModelAsController:
controller_dispatch:
- SilverStripe\CMS\Controllers\DefaultModelAsControllerDispatch
Custom yaml:
SilverStripe\CMS\Controllers\ModelAsController:
controller_dispatch:
- Me\MyApp\SomeDispatcher
Resulting calls:
hasControllerFor() is called on each dispatcher in order until it returns truecontrollerFor() is called on that dispatcherWith that in place we could provide a RemappedDispatch that had a config setting providing a number of namespace replacements to perform.
SilverStripe\CMS\Controllers\ModelAsController:
controller_dispatch:
- SilverStripe\CMS\Controllers\RemappedDispatch
SilverStripe\CMS\Controllers\RemappedDispatch:
namespace_mappings:
Me\MyApp\Models: Me\MyApp\Controllers
class_suffix: ""
You could also have a ModelSpecifiedDispatch that takes the coupled approach that was initially suggested:
<?php
/**
* Allow models to provide a getFrontendController() method for their class
*/
class ModelDrivenDispatch implements ModelAsControllerDispatch {
function hasControllerFor(DataObject $model)
{
return $model->hasMethod('getFrontendController');
}
function controllerFor(DataObject $model)
{
return $model->getFrontendController();
}
}
There's been a lot of talk about this. Currently in SS4 you can use controller_name config in your DataObject to control this behaviour. Is that enough to close this issue?
I think so
Most helpful comment
That's a good point. Strictly the ModelAsController would need a family of configuration that let is dispatch to the right controller based on the model that it looked up.
If this was done on an instance basis you'd be able to have two parallel websites based on the same SiteTree database, which would be interesting. It might lead to a better way of implementing things like multi-lingual sites and/or draft views.
You've already identified that simple rules could get too magical. However, another way to approach this would be to define a dispatcher interface that was tied into ModelAsController as a dependency:
Then you could have a few different implementations:
Controlleror_Controller(a configurable array of possible suffixes would work)To work with modules and the like you'd probably need to have a cascade of such handlers, which return
nullif they couldn't find a match.