XF 2.2 force lowercase links

briansol

Well-known member
I'm building a dynamic page which takes the url of the Title of a section. These will be admin-generated and all 1 word with no dupes, so i want to avoid adding IDs to the URL.

I'm struggling with that the link() tool in the template makes all links in sentence case and being on a linux server, i want to avoid all case senstitvity issues by making everything lowercase.

Is there a way to do this consistently by leveraging the link() function in the template? I don't see any other Xf or 3rd part mods passing in a pageurl as a param so i must be doing something wrong here...

/mymod/Title/ -> /mymod/title/
 

Kirby

Well-known member
I am not really sure what you are trying to do ...

Assuming that you have an entity with a field title and a corrresponding route with :str<title> you would most likely need a link building callback on your route as the default behaviour does not convert :str params to lowercase, see \XF\Mvc\Router::buildRouteUrl() and \XF\Mvc\Router::prepareStringForUrl()
 

briansol

Well-known member
Thanks, that's helpful.

What I still don't understand is how the native xf route for threads doesn't use the callback and yet thread titles are lowercase.
 

Jeremy P

XenForo developer
Staff member
The native routes typically use :int<some_id,title>, where the title string doesn't really matter since only the integer is used for look-ups. Since the title doesn't really matter, it is normalized (using \XF\Mvc\Router::prepareStringForUrl). Doing this automatically with :str<title> could create ambiguities in how to resolve string-based look-ups. For example, it would be possible for two distinct titles to be normalized to the same string. So you're left to implement normalization yourself in such a way to avoid these ambiguities.

For example, you could have a slug column with a unique index where the slug is run through \XF\Mvc\Router::prepareStringForUrl when saving. Your route format would be :str<slug> and link('route', $entity) would work like you'd expect.
 

briansol

Well-known member
Thanks, i'll play with this some more. the 'title' is from unique codes (think: part numbers) so even formatted shouldn't ever have slug issues. This will not be user-entered (admin and a few mods i trust)
 

Jeremy P

XenForo developer
Staff member
You could potentially use a slug getter (instead of a column) if you're absolutely sure there will be no collisions from normalization, though equally it's not a bad idea to account for human error. (Maybe accidental duplicate entries with part numbers which differ only in their capitalization, resulting in the same slug when normalized to lowercase, which a column with a unique constraint would catch.) Having a dedicated column with an index also makes database look-ups using the normalized slug trivial and fast.
 

briansol

Well-known member
Days later now and i'm stuck here again. buildRouteUrl is protected. I can't seem to leverage it from my class extension.

Any ideas?


Admin Route area:
added callback to my new buildURL method:

Code:
My\Mod\MVC\MyRouter :: buildURL

MyRouter looks like this:

Code:
<?php
namespace My\Mod\MVC;

use XF\MVC\Router;

class MyRouter extends Router {

    public static function buildURL($prefix, array $route, $action, $data = null, array &$parameters = [])
    {
        $url = (new Router)-buildRouteUrl($prefix, $route, $action, $data, $parameters);
        return $url;
    }
}
because buildRouteUrl is a protected function
An exception occurred: [Error] Call to undefined function My\Mod\MVC\buildRouteUrl() in src\addons\My\Mod\MVC\MyRouter.php on line 11

Without the static, it doesn't work at all.
without instantiating the class, all my routes die (forum won't load at all anywhere - why does this namespace effect the forum root? it's a 3rd party page and not extended any templates on the main site other than a nav bar entry)

So, is there a real-world example of extending this i can see? as i mentioned before, i can't find a single addon in the resources area that does anything like this.
 

Jeremy P

XenForo developer
Staff member
If you use the column/getter approach, you probably wouldn't need a callback at all.

Still, here is a reference for how you might use a route callback (Vendor\AddOn\Pub\Route\SomeController::build):
PHP:
<?php

namespace Vendor\AddOn\Pub\Route;

class SomeController
{
    public static function build(
        &$prefix,
        array &$route,
        &$action,
        &$data,
        array &$params,
        \XF\Mvc\Router $router
    ) {
        if ($data instanceof \Vendor\AddOn\Entity\SomeEntity) {
            $title = 'some-normalized-title'; // your logic goes here

            $data = [
                'title' => $title,
            ];
        }

        return null;
    }
}

Using link('route', $someEntity) would then give you route/some-normalized-title.
 
Last edited:

briansol

Well-known member
Doesn't seem to work, even with hard-coding in a string for the title into my route syntax (type) it never picks it up or seems to do anything.

Do i need to remove something in the route itself?

callback:
Code:
My\Mod\Pub\Route\MyRouter :: buildURL

Code:
:str<type>/:str<item>/:str<act>/

Code:
<?php

namespace My\Mod\Pub\Route;

class MyRouter
{
    public static function buildURL(&$prefix, array &$route, &$action, &$data, array &$params, \XF\Mvc\Router $router ) {
        if ($data instanceof \My\Mod\Entity\MyEntity)
        {       

\XF::dumpSimple($data);

            $type = 'test';

            $data = [
                'type' => $type,
            ];
        }
        return null;
    }
}

the dump doesn't print either.

template:

Code:
<xf:button href="{{ link('mybasepath', {'type': 'MyTitle', 'item': {$r.itemname}, 'act': 'add'}) }}"
class="button--cta" icon="write">{{ phrase('add') }}</xf:button>
 

Jeremy P

XenForo developer
Staff member
Haven't got a chance to test it myself right this second, but try dumping outside of the conditional to be sure $data is what you'd expect (an instance of \My\Mod\Entity\MyEntity).
 

Kirby

Well-known member
Code:
$url = (new Router)-buildRouteUrl($prefix, $route, $action, $data, $parameters);
because buildRouteUrl is a protected function

Without the static, it doesn't work at all.
without instantiating the class, all my routes die (forum won't load at all anywhere - why does this namespace effect the forum root? it's a 3rd party page and not extended any templates on the main site other than a nav bar entry)
Jeremy already gave you an example, but just for the reference:
You were getting the error because of missing >, so you did not call method buildRouteUrl but a function with that Name (which does not exist).
 

Jeremy P

XenForo developer
Staff member
Code:
<xf:button href="{{ link('mybasepath', {'type': 'MyTitle', 'item': {$r.itemname}, 'act': 'add'}) }}"
class="button--cta" icon="write">{{ phrase('add') }}</xf:button>

I've only just seen this, but for this to work as-is you'd want to pass in your whole entity as the second parameter. Something like:
{{ link('mybasepath/add', $r) }}

Or you can change the callback to suite your needs, what I posted was only an example. The callback gives you complete control over the prefix/route/action/data/parameters prior to the link being built.
 
Last edited:

briansol

Well-known member
So, if i have a callback defined, the route format set (eg, :str<type>) doesn't do anything?

I'm not seeing that happen at all... the opposite in fact. the route definition is the only thing that triggers.
 

Kirby

Well-known member
PHP:
<?php
namespace My\Mod\Pub\Route;

class MyRouter
{
    public static function buildURL(&$prefix, array &$route, &$action, &$data, array &$params, \XF\Mvc\Router $router )
    {
        if ($data instanceof \My\Mod\Entity\MyEntity)
        {
            return 'this/is/the/route/i/want/to/have/' . strtolower($data->title);
        }
    }
}
 

briansol

Well-known member
I'm still not getting anywhere with this.

I altered the return to match my route set,

Code:
return 'base/' . strtolower($data->type) .'/'. strtolower($data->item) .'/'. strtolower($data->act).'/';

$data instanceof does not match. i added an else case
Code:
else
        {
            \XF::dumpSimple('missed inst of');
        }

and it fires every time.


When i look at $data outside the conditional, it's populated.

But, I don't have 'type' or 'act' in my database and 'item' is a different field header depending on the table (eg, parts table its called partname, in the computer table its called computername, and so on). Could that be why?

When i change my template to

Code:
<xf:button href="{{ link('mybase', $r) }}"
class="button--cta" icon="write">{{ phrase('add') }}</xf:button>

All i get is mybase.


I tried to hard code an add:

Code:
<xf:button href="{{ link('mybase/ADD', $r) }}"
class="button--cta" icon="write">{{ phrase('add') }}</xf:button>

and used caps on purpose to see if it would reduce, and it does nothing.
 

Jeremy P

XenForo developer
Staff member
So, if i have a callback defined, the route format set (eg, :str<type>) doesn't do anything?
If you return a string from the callback, that will be used as the route. If you merely manipulate the passed arguments and return null from the callback, the format is still used to build the route. And in either case the format is used to build the parameters for inbound requests to the route.

and still with this format, accessing the protected function does not seem to be possible.
It's not meant to be, you shouldn't need to use \XF\Mvc\Router::buildRouteUrl at all.

$data instanceof does not match. i added an else case
...
and it fires every time.
Then you're not passing an instance of that entity as the second argument to the link function. What is $r? You'd have to adapt the code according to your needs.
 

briansol

Well-known member
am I missing something in this chain?

I have an entity class with structure and relations fully defined. As mentioned above my routing variables, 'type', 'item', and 'act', are not part of the table structure. Is this a problem?

I have a repository class that uses $finder -> pointing to the Entity.

I have a controller class that leverages the repository and ->fetch()'es the records into $fetcheddata.

it populates the template call $this->view with viewparams as 'mydata'->$fetcheddata.

my template collects $mydata and iterates through it as $r. I then call a macro to do each row. This all works and i get values in my template.

Code:
<xf:if is="$mydata is not empty">
    <xf:foreach loop="$mydata" value="$r">
          <xf:macro name="rrow"  arg-r="{$r}"  />
$r.id
$r.name
and so on all exist as per my entity structure. Again, i do not have type/item/act in this object as it's not in the db schema.

in the macro, i want to link to the details page.
Code:
<a href="{{ link('base', {'type': 'HARDCODED TYPE', 'item': {$r.name}}) }}">{$r.name}</a>

You said this is not going to work, so i replaced it with
Code:
<a href="{{ link('base/view', $r }}">{$r.name}</a>

and it doesn't work. I get .com/xf/base/ for all urls.

and if i use caps for VIEW, it returns all caps because the callback doesn't seem to match because the instanceof myentity doesn't match $data
 
Top