2

I am having a problem getting SilverStripe Fluent module to work with content/page controllers. Whenever a locale url segment is provided, the controller returns 404. For example, http://site.local/search works but http://site.local/en/search returns 404.

I tried using route config by pointing mi/search to the controller name. The template renders but the current locale is not correct.

To reproduce:

  1. Set up a SilverStripe project using composer create-project silverstripe/installer test
  2. Require the module composer require tractorcow/silverstripe-fluent
  3. Setup 2 locale
    • English with url segment 'en'
    • Maori with url segment 'mi'
  4. Create a simple controller called SearchController
  5. Create a route.yml in the config folder
  6. Create template file called Search.ss in the template folder
<?php
namespace App\Controllers;

use SilverStripe\Control\HTTPRequest;
use SilverStripe\CMS\Controllers\ContentController;

class SearchController extends ContentController
{

    private static $allowed_actions = [
        'index',
    ];

    public function index(HTTPRequest $request)
    {
        return $this->renderWith('Search');
    }
}
---
Name: approutes
After: framework/_config/routes#coreroutes
---
SilverStripe\Control\Director:
  rules:
    'search//': 'App\Controllers\SearchController'
#    'mi/search//': 'App\Controllers\SearchController'
#    'en/search//': 'App\Controllers\SearchController'
<h1>Search</h1>
$CurrentLocale

Navigate to <baseurl>/mi/search, the template should render:

<h1>Search</h1>
mi_NZ

But error 404 is returned.

scrowler
  • 24,273
  • 9
  • 60
  • 92
Phalkunz
  • 23
  • 2

1 Answers1

0

Concept

So Fluent supports localising via query string out of the box, but appending locales to the URL is reserved for pages driven by the CMS. With your example I'm able to see correct results via /search?l=en and /search?l=mi.

In order to allow locales in the URL for non-SiteTree routes, we can patch FluentDirectorExtension, which is the class responsible for injecting Fluent's routing rules, and add support for explicit configuration of routes that should also be localisable. This can be achieved by adding Director rules that essentially do the same thing as above, but masking the /search?l=en URL as /en/search in the background.

My example configuration is something like this:

TractorCow\Fluent\Extension\FluentDirectorExtension:
  static_routes: # Routes that should also allow URL segment based localisation
    - 'search//'

This should match the rule key in your Director.rules config.

We can then construct the new URL to allow support for, and tell Director to use the existing configured controller while also passing the l argument for the locale transparently. We need to do this for each locale, and the rules need to be inserted before Fluent's default rules are. An example of what you could do:

diff --git a/src/Extension/FluentDirectorExtension.php b/src/Extension/FluentDirectorExtension.php
index 6ebf1d6..0cdd80b 100644
--- a/src/Extension/FluentDirectorExtension.php
+++ b/src/Extension/FluentDirectorExtension.php
@@ -116,7 +116,10 @@ class FluentDirectorExtension extends Extension
     protected function getExplicitRoutes($originalRules)
     {
         $queryParam = static::config()->get('query_param');
+        $staticRoutes = static::config()->get('static_routes');
         $rules = [];
+        $prependRules = []; // we push these into the $rules before default fluent rules
+
         /** @var Locale $localeObj */
         foreach (Locale::getCached() as $localeObj) {
             $locale = $localeObj->getLocale();
@@ -138,8 +141,22 @@ class FluentDirectorExtension extends Extension
                 'Controller' => $controller,
                 $queryParam => $locale,
             ];
+
+            // Include opt-in static routes
+            foreach ($staticRoutes as $staticRoute) {
+                // Check for a matching rule in the Director configuration
+                if (!isset($originalRules[$staticRoute])) {
+                    continue;
+                }
+
+                $prependRules[$url . '/' . $staticRoute] = [
+                    'Controller' => $originalRules[$staticRoute],
+                    $queryParam => $locale,
+                ];
+            }
         }
-        return $rules;
+
+        return array_merge($prependRules, $rules);
     }

     /**

If you debug $rules at the end of the updateRules() method, you'll see that Fluent has now injected a new rule for that route in each locale:

  'en/search//' => 
    array (size=2)
      'Controller' => string 'App\Controllers\SearchController' (length=42)
      'l' => string 'en_NZ' (length=5)
  'mi/search//' => 
    array (size=2)
      'Controller' => string 'App\Controllers\SearchController' (length=42)
      'l' => string 'mi_NZ' (length=5)

Implementation

I'm going to formulate a pull request to the module for this change once I can back it up with some unit tests, but in the meantime, you can implement this by using an Injector override in your project code, and extend the protected getExplicitRoutes method to implement the changes above:

SilverStripe\Core\Injector\Injector:
  TractorCow\Fluent\Extension\FluentDirectorExtension:
    class: MyFluentDirectorExtension
class MyFluentDirectorExtension extends FluentDirectorExtension
{
    protected function getExplicitRoutes($originalRules)
    {
        $rules = parent::getExplicitRoutes($originalRules);

        $staticRoutes = static::config()->get('static_routes');
        $queryParam = static::config()->get('query_param');
        $prependRules = [];

        // Include opt-in static routes
        foreach (Locale::getCached() as $localeObj) {
            foreach ($staticRoutes as $staticRoute) {
                $locale = $localeObj->getLocale();
                $url = urlencode($localeObj->getURLSegment());

                // Check for a matching rule in the Director configuration
                if (!isset($originalRules[$staticRoute])) {
                    continue;
                }

                $prependRules[$url . '/' . $staticRoute] = [
                    'Controller' => $originalRules[$staticRoute],
                    $queryParam => $locale,
                ];
            }
        }

        return array_merge($prependRules, $rules);
    }
}
scrowler
  • 24,273
  • 9
  • 60
  • 92
  • Robbie, I think `$queryParam => $locale` shouldn't be there. The route works find but the `$CurrentLocale` in the test template doesn't show the selected locale - always `mi_NZ`. – Phalkunz Aug 28 '19 at 03:47
  • Sorry, I forgot to include the definition of `$queryParam` - have updated the example code to include it – scrowler Aug 28 '19 at 04:47