Silverstripe: Not able to access Page fields in controller with custom route - php

I'm using SilverStripe 3.3.1 and have a custom route set up to handle urls with many parameters. That works.
However, the routing rule causes Page fields and functions to be inaccessible in the Page_Controller and templates. Any ideas how to fix this?
//MyPage class
class MyPage extends Page {
//Not accessible if route to controller specified in config.yml
private static $db = array(
'MyPageVar' => 'Int',
);
//Not accessible if route to controller specified in config.yml
public function getMySpecialVar() {
return $this->MyPageVar;
}
}
//MyPage_Controller class
class MyPage_Controller extends Page_Controller {
private static $allowed_actions = array(
'index',
'detailsearch',
);
private static $url_handlers = array (
'detailsearch/$Key1/$Value1/$Key2/$Value2/$Key3/$Value3/$Key4/$Value4/$Key5/$Value5' => 'detailsearch',
);
/**
* UseMyPageVar()
*
* #return Boolean
*/
public function UseMyPageVar() {
//Empty if route to controller specified in config.yml
Debug::show($this->MyPageVar);
Debug::show($this->Title);
Debug::show($this->Content);
//Error if route to controller specified in config.yml
Debug::show($this->getMySpecialVar());
return true;
}
}
MyPage.ss
<!-- This work as expected if no route is specified. -->
<!-- But all vars are empty if route is specified in config.yml -->
<p>MyVar: $MyPageVar</p>
<p>Title: $Title</p>
<p>Content: $Content</p>
Routing rule in config.yml
Director:
rules:
'mypage': 'MyPage_Controller'
This question is also posted on the Silverstripe forum:
http://www.silverstripe.org/community/forums/general-questions/editpost/413506

It's not pretty, but for now I've solved the problem by using a private var in the Controller class to hold a reference to the page.
//MyPage_Controller class
class MyPage_Controller extends Page_Controller {
private $_page; //reference to page that's lost with custom routing
//ContentController uses route, which has been changed to
// 'MyPage_Controller' by routing rule, to initialize
// page reference. Can't find anything so reference
// not set. (set to -1)
public function init() {
parent::init();
//Initialize using default route overwritten in routing rule
// This will break if URL segment changed in CMS
$route = array_search($this->URLSegment,
Config::inst()->get('Director', 'rules'));
$link = str_replace($this->URLSegment, $route, $this->Link());
$this->_page = $this->Page($link);
}
//Use private var to access page fields
public function MyPageVar() {
Debug::show($this->_page->MyPageVar);
}
//expose $Content to templates
public function Content() {
return $this->_page->Content;
}
//Can't use Title() so expose Page Title as $PageTitle
public function PageTitle() {
return $this->_page->Title;
}
}

Three things spring to mind when I look at your code:
That "mypage" in config.yml should be the name of a public method on MyPage_Controller. As it is, SilverStripe cannot find a matching method called mypage and will default to calling index() instead.
Routes should really go in a separate routes.yml file so you can "namespace" it to be invoked before or after SilverStripe's own core routes. If you don't do this, then it may result in the weird behaviour you're experiencing.
Did you know that you can debug your routes using the ?debug_request=1 URL param? See: https://docs.silverstripe.org/en/3.3/developer_guides/debugging/url_variable_tools#general-testing

Related

How to create dynamic URLs at the base URL of a CMS?

I'm displaying user profiles on a PHP website using usernames as part of the URL that links to the given user profile.
I can achieve this through a controller, the ProfileController, but the URL will look like this thewebsite.com/profile/show_profile/ANYUSERNAMEHERE
What i want is something similar to Facebook, where the username is appended just after the base URL:
https://www.facebook.com/zuck
I tried passing a variable to the Index function (Index()) of the home page controller (IndexController), but the URL becomes thewebsite.com/index/ANYUSERNAMEHERE and the base url thewebsite.com throws an error:
Too few arguments to function IndexController::index(), 0 passed and exactly 1 expected.
The home page controller:
<?php
class IndexController extends Controller
{
public function __construct()
{
parent::__construct();
}
// IF LEFT, THE VARIABLE $profile THROWS AN ERROR AT THE BASE URL
public function index($profile)
{
/** AFTER REMOVING THE $profile VARIABLE ABOVE AND THE 'if'
* STATEMENT BELOW, THE ERROR THROWN AT THE BASE URL VANISHES AND
* THE WEBSITE GOES BACK TO IT'S NORMAL STATE. THIS CODE WAS USED
* TRYING TO RENDER THE URL thewebsite.com/ANYUSERNAMEHERE BUT IT
* ONLY WORKS WITH thewebsite.com/index/ANYUSERNAMEHERE
*/
if (isset($profile)) {
$this->View->render('profiles/show_profile', array(
'profiles' => ProfileModel::getSelectedProfile($profile))
);
} else {
$this->View->render('index/index', array(
'profiles' => ProfileModel::getAllProfiles()));
}
}
The profile controller:
<?php
class ProfileController extends Controller
{
public function __construct()
{
parent::__construct();
Auth::checkAuthentication();
}
public function index()
{
$this->View->render('profiles/index', array(
'profiles' => ProfileModel::getAllProfiles())
);
}
public function show_profile($profile)
{
if (isset($profile)) {
$this->View->render('profiles/show_profile', array(
'profiles' => ProfileModel::getSelectedProfile($profile))
);
} else {
Redirect::home();
}
}
}
I was expecting the base URL to pass the argument (the username) to the IndexController's Index($profile) function, but the webpage throws an error and the expected result is being displayed from the wrong URL: thewebsite.com/index/ANYUSERNAMEHERE
You would need to use a router based on regular expressions, like FastRoute, or Aura.Router.
For example, with FastRoute you'd define and add a route to the so-called route collector ($r) like this:
$dispatcher = FastRoute\simpleDispatcher(function(FastRoute\RouteCollector $r) {
// The /{profile} suffix is optional
$r->addRoute('GET', '[/{profile}]', 'handler');
});
where handler is just a generic name for a customizable route handler in form of a callable. For example, if you'd additionally use the PHP-DI/Invoker library, the route handler ('handler') could look like one of the following callables (at least):
[ProfileController::class, 'show_profile']
'ProfileController::show_profile'
So the complete route definition would be like:
$r->addRoute('GET', '[/{profile}]', [ProfileController::class, 'show_profile']);
$r->addRoute('GET', '[/{profile}]', 'ProfileController::show_profile');
The placeholder name (profile) corresponds to the name of the parameter of the method ProfileController::show_profile:
class ProfileController extends Controller {
public function show_profile($profile) {
...
}
}
Even though the URL would look like you want it, e.g. thewebsite.com/zuck, I imagine that the placeholder {profile} of the above route definition would come in conflict with the fixed pattern parts defined in other route definitions, like /books in:
$r->addRoute('GET', '[/books/{bookName}]', 'handler');
So I suggest to maintain a URL of the form thewebsite.com/profiles/zuck, with the route definition:
$r->addRoute('GET', '/profiles/{profile}', 'handler');
I also suggest to read and apply the PHP Standards Recommendations in your code. Especially PSR-1, PSR-4 and PSR-12.

Empty route leeds to 404-Error - SilverStripe 3.5

Like superficial descriped in SilverStripe Docs, I'm trying to set a custom controller for my homepage.
I changed the default homepage link to 'custom-home' and added those two routes.
The second one, with the path in it works and directs me to my controller. The first (empty) one just sends me to an 404-error page.
Couldn't figure out how to fix that. Any suggestions?
routes.yml
Director:
rules:
'': 'MyHome_Controller'
'custom-home': 'MyHome_Controller
_config.php
RootURLController::set_default_homepage_link('custom-home');
MyHome_Controller.php
<?php
class MyHome_Controller extends Page_Controller {
private static $allowed_actions = [];
private static $url_handlers = [];
public function init() {
parent::init();
}
public function Link($action = null) {
return Director::baseURL() . 'custom-home';
}
public function index() {
$data = [
'Title' => 'Hello World',
'ClassName' => __CLASS__,
];
return $this
->customise($data)
->renderWith([__CLASS__, 'Page']);
}
}
I believe the way the empty route (RootURLController) works is that you're telling it the URLSegment of a page in the CMS that should resolve to the root URL. So I think what you need to do is go into the CMS and change the URLSegment of your CustomHomePage to 'custom-home'.

Calling an action on ModelAsController subclass

I have a custom route in my routes.yml that forwards any unknown request to BaseController
'$URLSegment/$Name/$Action/$ID': 'BaseController'
And from there the the request is routed (among other places) to my main Controller where it is handled by the index() of the controller.
However it will always use the index() so if I go to the URL test1/test2/action/5 it will still be run by index()
This is my BaseController
class BaseController extends ModelAsController {
public function getNestedController() {
$action
$params = $this->getRequest()->params();
$this->loadMain($params['URLSegment'], $params['Name'], $params['Action'], $params['ID']);
}
private function loadMain($first, $name, $action, $id) {
$main = new MainController();
$main->{$action}();
}
}
and this will call the function however the index() function has already run and set the template.
I know I could call the function from index() and return the template like that however I'm fairly sure this would bypass the security features of the allowed actions which I'm keen to maintain.
I've defined $allowed_actions in my MainController and have added:
private static $url_handlers = array(
'$URLSegment/$Name/something/$ID' => 'something'
);
but it still just calls index().
How can I maintain the same action routing of SilverStripe through the custom routed MainController
You need to define private static $allowed_actions and maybe private static $url_handlers also, the latter providing custom routing.
Basically all the things from: https://docs.silverstripe.org/en/3/developer_guides/controllers/routing/
This isn't well documented but if you are wanting to reverse the URL convention of /path/$Action/$Name/$ID to something like /path/$Name/$Action then you put:
private static $url_handlers = array(
'path//$Name/$Action' => 'handleAction'
);
The handleAction is important as it calls the parents (Controller`) handleAction and routes it correctly applying permissions and so forth.

Controller routing not working as expected in silverstripe 3.1

I'm setting up routing to a controller and I keep getting either a 404, or the 'getting started with the silverstripe framework' page.
In routes.yaml I have:
---
Name: nzoaroutes
After: framework/routes#coreroutes
---
Director:
rules:
'view-meetings/$Action/$type': 'ViewMeeting_Controller'
My controller looks like this:
class ViewMeeting_Controller extends Controller {
public static $allowed_actions = array('HospitalMeetings');
public static $url_handlers = array(
'view-meetings/$Action/$ID' => 'HospitalMeetings'
);
public function init() {
parent::init();
if(!Member::currentUser()) {
return $this->httpError(403);
}
}
/* View a list of Hospital meetings of a specified type for this user */
public function HospitalMeetings(SS_HTTPRequest $request) {
print_r($arguments, 1);
}
}
And I've created a template (ViewMeeting.ss) that simply outputs $Content, but when I flush the site cache and visit /view-meetings/HospitalMeetings/6?flush=1
I get the default 'getting started with the Silverstripe framework' page
I know the routing in routes.yaml is working, because if I change the route there and visit the old URL I get a 404, but the request doesn't seem to fire my $Action...
You had 2 different rules in your YAML and controller ($type vs $ID). Also, I don't think you need to define the route in both YAML and the Controller.
Try this, the YAML tell SS to send everything that starts with 'view-meetings' to your Controller, then $url_handlers tell the Controller what to do with the request depending on everything after 'view-meetings' in the URL.
routes.yaml
---
Name: nzoaroutes
After: framework/routes#coreroutes
---
Director:
rules:
'view-meetings': 'ViewMeeting_Controller'
ViewMeeting_Controller.php
class ViewMeeting_Controller extends Controller {
private static $allowed_actions = array('HospitalMeetings');
public static $url_handlers = array(
'$Action/$type' => 'HospitalMeetings'
);
public function init() {
parent::init();
if(!Member::currentUser()) {
return $this->httpError(403);
}
}
public function HospitalMeetings(SS_HTTPRequest $request) {
}
}
The Silverstripe documentation on routing isn't at all clear on this point, but for $Action to be correctly interpreted you should use a double slash before it in the routes.yml file:
view-meetings//$Action/$type
According to the documentation, this sets something called the 'shift point'. Exactly what this means isn't described very well either in the documentation or in the source code which matches URLs against rules.
I'm doing some guessing here, but what if you drop the
public static $url_handlers = array(
'view-meetings/$Action/$ID' => 'HospitalMeetings'
);
part and change the Action method to:
// View a list of Hospital meetings of a specified type for this
public function HospitalMeetings(SS_HTTPRequest $request) {
// Should print 8 if url is /view-meetings/HospitalMeetings/6
print_r($request->param('type');
}

Rendering a different Kohana template at the action level

I have an account controller with a template called 'account'. I'm just trying to figure out how to specify a different template for my login / forgot password actions.
I'm assuming your controller is extended Controller_Template? In the controllers before method, you could check the name of the action and change the template based on that:
<?php
class Controller_Account extends Controller_Template {
// This is the default template used for all actions
public $template = 'account';
public function before()
{
// You can add actions to this array that will then use a different template
if (in_array($this->request->action(), array('login', 'forgot_password')))
{
$this->template = 'diff_template';
}
parent::before();
}

Categories