XF 2.2 Back-tracing how a variable got into a template

eDaddi

Active member
Curious how XF devs quickly find out how a variable became available in a template... and make it available in other places.

My goal is to get available user upgrades in a widget to display in their related forum's sidebar.

Specifically the 'available' variable thats accessible on the Account Upgrades page/template
Screen Shot 2022-06-07 at 3.22.27 PM.png


How do you find the callback that made the var available there?
 
Last edited:
If you look into the controller XF\Pub\Controller\Account, then you will see, that the method actionUpgrades() passes the variable $available to the template account_upgrades:

PHP:
        $upgradeRepo = $this->repository('XF:UserUpgrade');
        list ($available, $purchased) = $upgradeRepo->getFilteredUserUpgradesForList();
 
For anyone interested in editing templates, digging into the code, etc. Visual Studio Code is free and very easy to use.

You can search all of the files for specific methods, variables, etc.

 
If you look into the controller XF\Pub\Controller\Account, then you will see, that the method actionUpgrades() passes the variable $available to the template account_upgrades:

PHP:
        $upgradeRepo = $this->repository('XF:UserUpgrade');
        list ($available, $purchased) = $upgradeRepo->getFilteredUserUpgradesForList();
I found that using 'find in files' search ... I'm curious how you traced it back to that? Just experience?

I don't see where that function is call from, how is that triggered in the 'account_upgrades' template and how can I do it for the custom widget's template?

For anyone interested in editing templates, digging into the code, etc. Visual Studio Code is free and very easy to use.

You can search all of the files for specific methods, variables, etc.

That is a nice app, I've been on Sublime Text for so long I'm afraid to try something new lol
 
I found that using 'find in files' search ... I'm curious how you traced it back to that? Just experience?
The subpages of your XF forum are served by controllers. You find the controller classes (XF core only, of course) in the folder src/XF/Pub/Controller. Most variables (except $xf and maybe some other global variables) are passed to the template as "view parameters".

Now there are a few ways to track that down:
  • search for 'variablename' (with single quotes!) in the controller folder.
  • search for the template name in the controller folder. This makes only sense if it is not a sub-template you are dealing with; in that case you have to find the main template by searching for the template name you know in all templates' content.
  • Most times you will see Controller class and action by the route. You can also look at the routes list (admin.php?routes/) in order to find the right controller class.
I don't see where that function is call from, how is that triggered in the 'account_upgrades' template and how can I do it for the custom widget's template?
see dev docs:


 
Last edited:
see dev docs:


It seemed simple ...

<xf:callback class="XF\Pub\Controller\Account" method="actionUpgrades" params="[]"></xf:callback>

but I guess since 'action' is not a callback method I guess I can't go that route. I guess I need my own add on to call the UserUpgrade method.
 
but I guess since 'action' is not a callback method I guess I can't go that route.
no. If you read the docs, you should know what a Controller and an action is. They usually return a view (if you are working with classes and methods, always look at the return values).

I guess I need my own add on to call the UserUpgrade method.
no, you can do this within a template:

HTML:
<xf:set var="$upgrades" value="{$xf.app.em.getRepository('XF:UserUpgrade').getFilteredUserUpgradesForList()}" />

<xf:set var="$available" value="{$upgrades.0}" />
<xf:set var="$purchased" value="{$upgrades.1}" />

<xf:set var="$profiles" value="{$xf.app.em.getRepository('XF:Payment').getPaymentProfileOptionsData()}" />

<xf:js src="xf/payment.js" min="1" />

<xf:if contentcheck="true">
    <xf:contentcheck>

        <xf:if is="$available is not empty">
            <div class="block">
                <div class="block-container">
                    <h2 class="block-header">{{ phrase('available_upgrades') }}</h2>

                    <div class="block-body">
                    <xf:foreach loop="$available" value="$upgrade">
                        <xf:formrow rowtype="button"
                            label="{$upgrade.title}"
                            hint="{$upgrade.cost_phrase}"
                            explain="{$upgrade.description|raw}">

                            <xf:form action="{{ link('purchase', $upgrade, {'user_upgrade_id': $upgrade.user_upgrade_id}) }}" ajax="true" data-xf-init="payment-provider-container">
                                <div class="inputGroup">
                                    <xf:if is="{{ count($upgrade.payment_profile_ids) > 1 }}">
                                        <xf:select name="payment_profile_id">
                                            <xf:option>{{ phrase('(choose_payment_method)') }}</xf:option>
                                            <xf:foreach loop="$upgrade.payment_profile_ids" value="$profileId">
                                                <xf:if is="{$profiles.{$profileId}}">
                                                    <xf:option value="{$profileId}">{$profiles.{$profileId}}</xf:option>
                                                </xf:if>
                                            </xf:foreach>
                                        </xf:select>

                                        <span class="inputGroup-splitter"></span>

                                        <xf:button type="submit" icon="purchase" />
                                    <xf:else />
                                        <xf:button type="submit" icon="purchase" />

                                        <xf:hiddenval name="payment_profile_id">{$upgrade.payment_profile_ids|first}</xf:hiddenval>
                                    </xf:if>
                                </div>
                                <div class="js-paymentProviderReply-user_upgrade{$upgrade.user_upgrade_id}"></div>
                            </xf:form>

                        </xf:formrow>
                    </xf:foreach>
                    </div>
                </div>
            </div>
        </xf:if>

        <xf:if is="$purchased is not empty">
            <div class="block">
                <div class="block-container">
                    <h2 class="block-header">{{ phrase('purchased_upgrades') }}</h2>

                    <ul class="block-body listPlain">
                    <xf:foreach loop="$purchased" value="$upgrade">
                        <li>
                            <div>
                                <xf:set var="$active" value="{$upgrade.Active.{$xf.visitor.user_id}}" />
                                <xf:formrow
                                    label="{$upgrade.title}"
                                    hint="{$upgrade.cost_phrase}"
                                    explain="{$upgrade.description|raw}">

                                    <xf:if is="$active.end_date">
                                        {{ phrase('expires:') }} <xf:date time="{$active.end_date}" />
                                    <xf:else />
                                        {{ phrase('expires_never') }}
                                    </xf:if>

                                    <xf:if is="$upgrade.length_unit && $upgrade.recurring && $active.PurchaseRequest">
                                        <xf:set var="$provider" value="{$active.PurchaseRequest.PaymentProfile.Provider}" />
                                        {{ $provider.renderCancellation($active)|raw }}
                                    </xf:if>
                                </xf:formrow>
                            </div>
                        </li>
                    </xf:foreach>
                    </ul>
                </div>
            </div>
        </xf:if>
    </xf:contentcheck>
<xf:else />
    <div class="blockMessage">{{ phrase('there_currently_no_purchasable_user_upgrades') }}</div>
</xf:if>
First we use <xf:set> to set the variables we need ($available, $purchased, $profiles). We use the appropriate repositories in order to do this (like the Account Controller does).

Then I included the content of the account_upgrades template. It will look ugly as a widget, so you will have to change the look, but you have everything there you need.
 
no. If you read the docs, you should know what a Controller and an action is. They usually return a view (if you are working with classes and methods, always look at the return values).
I did read them but that didn't sink in.
PHP:
return $this->view
I see that and it makes sense. I was assuming that, somewhere, php was looking at which callback method was prepended to the function's name, and since 'action' wasn't on that list that is why I was getting the error I was.

HTML:
<xf:set var="$upgrades" value="{$xf.app.em.getRepository('XF:UserUpgrade').getFilteredUserUpgradesForList()}" />

<xf:set var="$available" value="{$upgrades.0}" />
<xf:set var="$purchased" value="{$upgrades.1}" />

<xf:set var="$profiles" value="{$xf.app.em.getRepository('XF:Payment').getPaymentProfileOptionsData()}" />
I assume this portion is unnecessary in the
Code:
account_upgrades
template because of this:
PHP:
        $paymentRepo = $this->repository('XF:Payment');
        $profiles = $paymentRepo->getPaymentProfileOptionsData();

        $viewParams = [
            'available' => $available,
            'purchased' => $purchased,
            'profiles' => $profiles
        ];
        $view = $this->view('XF:Account\Upgrades', 'account_upgrades', $viewParams);
in the account.php file?

For the sake of argument, and others stumbling on this later, if this:
PHP:
 $view = $this->view('XF:Account\Upgrades', 'account_upgrades', $viewParams);
was
PHP:
 $view = $this->view('XF:Account\Upgrades', '_myWidgetTemplateName', $viewParams);
and the routing hit still hit that we wouldn't need to define the vars in the template?
 
I assume this portion is unnecessary in the
Code:
account_upgrades
template because of this:
yeah. The code you quoted plus the code, that uses the UserUpgrade repository. So the controller fetches the data and passes it into the template, so it is usable in the template.

For the sake of argument, and others stumbling on this later, if this:
PHP:
$view = $this->view('XF:Account\Upgrades', 'account_upgrades', $viewParams);
was
PHP:
$view = $this->view('XF:Account\Upgrades', '_myWidgetTemplateName', $viewParams);
and the routing hit still hit that we wouldn't need to define the vars in the template?
No, widgets usually don't return a view. You have to add a "Widget definition" in the admin cp and create a widget class, that extends XF\Widget\AbstractWidget. This class has to have the method render() (at least). See XF\Widget\OnlineStatistics for reference:

PHP:
<?php

namespace XF\Widget;

class OnlineStatistics extends AbstractWidget
{
    public function render()
    {
        /** @var \XF\Repository\SessionActivity $activityRepo */
        $activityRepo = $this->repository('XF:SessionActivity');

        $viewParams = [
            'counts' => $activityRepo->getOnlineCounts()
        ];
        return $this->renderer('widget_online_statistics', $viewParams);
    }

    public function getOptionsTemplate()
    {
        return null;
    }
}
widget_online_statistics is the template name in this case.

If you want to use a callback method ("PHP Callback" widget) it works similar:

Example: AddOn\Path\To\ClassName :: methodName

Specify a PHP callback here that can be used to render your widget.

Callback arguments:
  1. \XF\Widget\AbstractWidget $widget
    This widget. From this you can access the WidgetConfig object and the \XF\App object in order to fetch data and render a template by returning the $widget->renderer() object.
 
Top Bottom