Skip to main content

This site requires you to update your browser. Your browsing experience maybe affected by not having the most up to date version.

We've moved the forum!

Please use forum.silverstripe.org for any new questions (announcement).
The forum archive will stick around, but will be read only.

You can also use our Slack channel or StackOverflow to ask for help.
Check out our community overview for more options to contribute.

Template Questions /

Moderators: martimiz, Sean, Ed, biapar, Willr, Ingo, swaiba

Controller Functions Parameters


Go to End


4 Posts   5841 Views

Avatar
kaanuni

Community Member, 22 Posts

10 October 2012 at 3:31am

Edited: 10/10/2012 3:43am

Hi,

This is my first post on the forums. I've been learning silverstripe for a while, and have started my first production website on it. I have an issue that has been bugging me for months and I can't seem to resolve on my own. So I decided to post here and ask for help. First some code:

these are the relevant parts of page.php

class Page extends SiteTree {

	public static $db = array(
        'ShowChildren' => 'Boolean',
        'IsHotel' => 'Boolean',
        'HotelCategory' => 'Varchar',
        'HotelBoard' => 'Varchar',
        'NChildren' => 'Int'
	);
...

class Page_Controller extends ContentController {

	public static $allowed_actions = array (
	);

	public function init() {
		parent::init();
	}

        public function getHotelStars() {
            if($this->data()->IsHotel) {
                $hc = $this->data()->HotelCategory;
            
                if( preg_match( '/(\*+)(\+?)/', $hc, $matches ) ) {
                    $stars = strlen($matches[1]);
                    $halfStar = $matches[2] == '+';
                } else if( preg_match( '/(\d)(\++)/', $hc, $matches ) ) {
                    $stars = $matches[1];
                    $halfStar = $matches[2] == '+';
                } else if( preg_match( '/(\d)(\.5)?/', $hc, $matches ) ) {
                    $stars = $matches[1];
                    $halfStar = $matches[2] == '.5';
                }

                $hc = array_fill( 0, $stars, array( 'star' => '*' ));
                if ( $halfStar ) array_push( $hc, array( 'star' => '+' ));
                return new DataObjectSet($hc);
            
            } else return '';
        }

and this is the relevant bit of page.ss:

            <% if IsHotel %>
            <% loop $HotelStars %>
            <% if star = '*' %>
            <img src="$ThemeDir/images/star.png" />
            <% else %>
            <img src="$ThemeDir/images/halfstar.png" />
            <% end_if %>
            <% end_loop %>
            <% end_if %>

Ok so what I've been doing here is try and predict every possible value a user can add to the property $HotelCategory and parse inside the controller function HotelStars into a standard format: Array( '*', '*', '*', '+') for a hotel with 3.5 stars. I used the array format so it can be looped through in the template and I can keep the relevant img tags in the template.

It works too. Well, it works as long as you are printing out the stars on the same page. If you are looping through child pages, it doesn't work. It took me a while to figure out why, but now I understand that the controller functions are not accessible within that context. I can call the controller function of the parent page using up or top but that only returns the category (if it exists) of the top page. So I modified the function as such:


    public function getHotelStars($hc = -1) {
        if($hc != -1 || $this->data()->IsHotel) {
            if( $hc == -1 ) $hc = $this->data()->HotelCategory;
            
            if( preg_match( '/(\*+)(\+?)/', $hc, $matches ) ) {
                $stars = strlen($matches[1]);
                $halfStar = $matches[2] == '+';
            } else if( preg_match( '/(\d)(\++)/', $hc, $matches ) ) {
                $stars = $matches[1];
                $halfStar = $matches[2] == '+';
            } else if( preg_match( '/(\d)(\.5)?/', $hc, $matches ) ) {
                $stars = $matches[1];
                $halfStar = $matches[2] == '.5';
            }

            $hc = array_fill( 0, $stars, array( 'star' => '*' ));
            if ( $halfStar ) array_push( $hc, array( 'star' => '+' ));
            return new DataObjectSet($hc);
            
        } else return '';
    }

Now theoretically, I should be able to pass a string argument to the function and it will use that instead of $HotelCategory. Then inside the template I could do this:

     <% loop $PaginatedChildren %>
            <% if IsHotel %>
            <% loop $Top.HotelStars($HotelCategory) %>
            <% if star = '*' %>
            <img src="$ThemeDir/images/star.png" />
            <% else %>
            <img src="$ThemeDir/images/halfstar.png" />
            <% end_if %>
            <% end_loop %>
            <% end_if %>

But this doesn't work. Not only can I not pass $HotelCategory to the function, I can't pass a literal, not even in the regular scope. A simple:

$HotelStars(5)
$HotelStars('5')
$HotelStars("5")

Does nothing adding an echo $hc to the beginning of the function just prints -1 regardless of what i put in the parentheses.

So why is this? The documentation seems to indicate that I can pass parameters to controller functions. What am I doing wrong?

Having browsed through some forum posts I have come to the conclusion that I can't pass properties as parameters anways and that would kill this particular implementation. But I am still curious as to why I can't seem to pass a literal.

Beyond this, what is the best method of pre processing database values? Should I be incorporating the controller function into the model? Probably the best way to go about this is to sanitize and standardize the database entries for $HotelCategory after input in the cms. IMHO the most elegant way to do this would be with an accessor function setHotelCategoy. But I am not sure they exist in silverstripe. So I am asking if silverstripe detects such accessor functions.

I assume there is also a way to add a preprocessing function for input values in the getCMSFields method. Something along the lines of:

        $fields->addFieldToTab('Root.Hotel', new TextField('HotelCategory', 'Category', preprocessor));

But I don't think this would be as elegant as accessor functions. Either way there has to be a way to turn whatever value I end up storing in the database (either 3.5 or '***+') into images and to do this in the template. So I would either have to be able to split the string into an array or use a string replacement function in the template or use something like a traditional for loop for the float. How would I go about either one of these? Either that or I would have to be able to store a DataObjectSet in $db.

So to sum up this is what I am asking:

1- Why can't I pass a parameter to my controller function?
2- Can I pass a db property as a parameter to a controller function?
3- Does silverstripe have a way of defining accessor functions for db properties? Or how would I add a function to sanitize the inputs, during backend field deceleration.
4- Are there any functions available in the template that would allow for string replacement, an equivalent of the php explode function or a traditional for loop?
5- Can I store a DataObjectSet in the $db.

Thanks.

Avatar
kaanuni

Community Member, 22 Posts

12 October 2012 at 11:36pm

Edited: 12/10/2012 11:39pm

Ok, so I figured out my issue. Turns out silverstripe has these things called virtual properties and if I just move the getHotelStars method from the controller to the model (and change $this->data() to $this), I can reference it with $HotelStars in the template, and it will work within looping contexts.

So this solves my main issue. But I still have questions if anyone could give me an answer I would be eternally grateful:

1- Why can't I pass a parameter to my controller function?
2- Can I pass a db property as a parameter to a controller function?
3- Are there any functions available in the template that would allow for string replacement, an equivalent of the php explode function or a traditional for loop?
4- Can I store a DataObjectSet in the $db.
5- Can I use virtual property methods in FormField declerations the getCMSFields method? If yes, how? In my example would I just associate a FormField with 'HotelStars'?
6- What happens if I declare virtual property methods for a property that already exists? Do they then work like accessor functions?

Hopefully someone with experience will answer, but I will experiment anyway. So If I figure these out before somebody replies than I'll post my results here and hopefully they will help someone else.

Avatar
Willr

Forum Moderator, 5523 Posts

13 October 2012 at 3:25pm

1. You can pass parameters. I think there is a bug related to 2.4 with parameters and using get* so try naming your function HotelStars and using $HotelStars(5) will work (as long as your scope is correct).

2. In 2.4 you can't pass variables into functions ($Foo($Bar)) but in 3.0 this is possible.

3. String replacement should be done on your model, keep your templates clean and accessible. I'd define something like this on your SomeObject class - function getReplacedTitle() { return str_replace(..); } Then use $ReplacedTitle instead of $Title.

4. A DataObjectSet is a collection of database records, you really don't need to 'store'. If you need to store a list of data in the database store it as a Text field as a serialised object.

5. Sure, as long as your function is on your model class you can refer to it from wherever you're in that scope.

6. Yes, overrides the getter for the database.

Avatar
kaanuni

Community Member, 22 Posts

24 October 2012 at 3:51am

Thank you Willr for your response.

1 & 2. In retrospect I don't think its a bug. You wouldn't really expect an accessor to take a parameter, and the error persists in 3.0. Since I already solved my issue by moving my function to the model (where in retrospect I believe it belongs), I created a test function called square ($param){return $param*$param;} and tested it. Not only did it accept parameters when placed in both the model and the controller but it took model properties as well as arguments and with a little modification multiple parameters.

I haven't tested out 5 & 6, but will be doing so in the next few weeks when I have time. If, as I think based on your response, it is super easy, then that is awesome.

As to points 3. and 4. While I agree that string replacement should not be done in the template, it might have looked cleaner than what I have now, which is a hack, and would have been a million times better than having literal html and hard coded image links in the model, which is what would need to be done if I were to use string replacement for this purpose in the model. What I use now is a ArrayList (because when I turned on dev mode, silverstripe complained that dataobjectset is obsolete) generated in the model 7from a string or a number. This way I can loop over it in the template and insert the pictures in the templates. But it is definitely a hack, and too computationally intensive for my purposes.

What I would really like to be doing is defining all the conversion in the setter; so that it only happens once when the value is entered into the database and then put something like this in the template:

<% repeat $stars %>
<img src="star.png" />
<% end_repeat %>

I've searched through the documentation for such a template command to no avail. Am I missing something or should I file a feature request?

More specifically I think syntax would be something like this:

<% repeat $integer_expression %>
$iterator
<% end_repeat %>

It seems to me like this wouldn't cost an arm and a leg in time to implement and would be useful, i'm sure for other things besides rating images for hotels. I can think of a few other use cases that would benefit:

- Besides hotel ratings there are a bunch of other possible uses for repeated images: the number of doors an automobile has and it's maximum occupancy; maximum occupancy for hotel rooms; ratings for books, films, blog posts, comments, users.
- A page that has a db specified number of interchangeable dynamic DOM objects that the user can interact with, for example n color swatches or n dragable objects.
- Placeholders for a known number of content items loaded dynamically by javascript.

I'm sure there are more as well, and I am sure there are other ways of implementing all of them. For example the best way with current syntax for an integer rating between 1 and 5 would be to have 5 seperate images for the 5 possible ratings and do something like this:

<img src="$ThemeDir/images/rating_$rating.png" />

And I might end up implementing this for my site to save on processor power. But I've never heard of a platform that lost market share because it was too flexible.