Symfony4で_locale入りのURLにいい感じで対応したくて色々やったのでメモ
やりたかったこと
- 日本語と英語に対応したサイトで
- URLの
_locale
パラメーターで言語を切り替えられるようにしつつ -
_locale
の指定を省略したURLでもアクセスできる(デフォルトロケールになる)ようにし - なおかつデフォルトロケールを明示的に指定したURLにアクセスされた場合は
_locale
を省略したURLにリダイレクトする
具体的に言うと、
URL | 期待する動作 |
---|---|
/ |
日本語(デフォルトロケール)でトップページが表示される |
/path/to/page |
日本語(デフォルトロケール)でページが表示される |
/ja/ |
/ にリダイレクトされる |
/ja/path/to/page |
/ にリダイレクトされる OR /path/to/page にリダイレクトされる |
/en/ |
英語でトップページが表示される |
/en/path/to/page |
英語でページが表示される |
みたいな感じ。
デフォルトロケールの場合はURLにja
を入れたくない、という動機。
結論
- 不可能
- トップページしか存在しないサイトではこれで上手くいってるように見えるけど、下層ページがあると詰む
- やりたいことをやるには
_locale
部分に空文字を許可する必要があるけど、それをやると後に続くパスの1つ目が_locale
と誤認されてしまうため -
/en
をURLの末尾につけるという仕様なら可能な気がする(試してない) - ローカルでの動作確認とかがちょっとだけ面倒になるけど、ホスト名を
hoge.com
とen.hoge.com
とかで分けるのが簡単できれいな気がする
顛末
ロケール対応する前のコード
# config/routes/annotations.yaml
controllers:
resource: ../../src/Controller/
type: annotation
// src/Controller/HomeController.php
namespace App\Controller;
/**
* @Route("/", name="home_")
* @Template()
*/
class HomeController extends Controller
{
/**
* @Route("/", name="index")
*/
public function index()
{
return [];
}
}
やったこと
一手目
# config/routes/annotations.yaml
+ default_locale_redirection:
+ path: '/%locale%/{any}'
+ controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction
+ requirements:
+ any: .*
+ defaults:
+ path: /
+ permanent: true
controllers:
resource: ../../src/Controller/
type: annotation
+ prefix: /{_locale}/
+ requirements:
+ _locale: ja|en|.*
- 下半分でまずロケールを指定しないURLを許可
-
ja|en|.*
ではなく単に.*
としてしまうと/en/
にアクセスしてもロケールがセットされない - 上半分でデフォルトローケルを指定されたURLへのアクセスをロケール指定なしのトップページへリダイレクト
-
/ja/
以下に続くパスを取得して後方参照よろしくリダイレクト先のURLにくっつけることができたら最高だったけどそんな手段はなさそうだった
参考:https://symfony.com/doc/current/routing/redirect_in_config.html
→結局先述した理由でNGだった
二手目
# config/routes/annotations.yaml
default_locale_redirection:
path: '/%locale%/{any}'
controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction
requirements:
any: .*
defaults:
path: /
permanent: true
- controllers:
+ controllers_with_locale:
resource: ../src/Controller/
type: annotation
prefix: /{_locale}/
- requirements:
- _locale: ja|en|.*
+ condition: "request.geRequestUri() matches '#^/(ja|en)#'"
+ controllers_without_locale:
+ resource: ../src/Controller/
+ type: annotation
+ condition: "not (request.geRequestUri() matches '#^/(ja|en)#')"
condition
を使うにはcomposer require expression-language
が必要
とか迷走気味にやってみて、要は
- リクエストURIが
/ja
または/en
で始まるリクエストはcontrollers_with_locale
設定に - リクエストURIがそれ以外で始まるリクエストは
controllers_without_locale
設定に
それぞれ処理させるという人知を超えた荒技が使えないかと思ったのだけど、ルーティング設定ってそういうものじゃないし普通に無理だった。
requirements
やcondition
は「その条件に当てはまらなかったらマッチしない」というだけで、マッチしなかったら次のルーティング設定がマッチするか試行されるみたいなことはない。常に後に書かれているルーティング設定が勝つ。(体験談。要出典)
マッチしなかった場合の扱いは
# config/packages/routing.yaml
framework:
router:
strict_requirements: ~
これ次第。(あんまり詳しく調べてない)
参考:
https://symfony.com/doc/current/routing/conditions.html
https://symfony.com/doc/current/components/expression_language/syntax.html
三手目
ググってたら /{param1}{slash}{param2}
的なルート定義を駆使してなんかハックしてる人を見つけた(URLメモるの忘れてて逸失)ので、参考にして以下のようにしてみた。
# config/routes/annotations.yaml
default_locale_redirection:
path: '/%locale%{any}'
controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction
requirements:
any: .*
defaults:
path: /
permanent: true
controllers:
resource: ../../src/Controller/
type: annotation
+ prefix: /{_locale}{slash_or_none}
+ defaults:
+ _locale: '%locale%'
+ slash_or_none: ~
+ requirements:
+ _locale: ja|en|
+ slash_or_none: /?
これによってロケールを上手く取得することまではできた。
けど、「デフォルトロケール指定のURLだった場合はロケール指定なしのURL のトップページ にリダイレクト」という処理が残っている、アプリの仕様によっては予期せぬリダイレクトループが発生しうることが分かった(というか作ってたアプリで発生して気づいた)ので、そのリダイレクトは諦めてサーバーにやってもらうことにした。
たぶん、多くのアプリではこの方法で特に問題なく動くと思う。
四手目
サーバーでリダイレクトするなら、アプリ内のルーティングの知識を漏らしたくないのでロケールはホスト名に入れることにした。
Symfony側のルーティング設定は↑と同じ考え方で割と簡単に行けた。
# config/routes/annotations.yaml
controllers:
resource: ../../src/Controller/
type: annotation
+ host: '{_locale}{dot_or_none}%domain%'
+ defaults:
+ _locale: ~
+ dot_or_none: .
+ requirements:
+ _locale: ja|en|
+ dot_or_none: \.?
# /etc/nginx/conf.d/server.conf
server {
listen 80;
server_name ja.hoge.com;
return 301 $scheme://hoge.com$request_uri;
}
参考:https://symfony.com/doc/current/routing/hostname_pattern.html
やらなかったこと
@Route
アノテーションで対応
コントローラーの// src/Controller/HomeController.php
namespace App\Controller;
/**
* @Route("/", name="home_")
* @Template()
*/
class HomeController extends Controller
{
/**
* @Route("{_locale}/", name="index")
* @Route("/", defaults={"_locale" = "ja"})
*/
public function index()
{
return [];
}
}
ドキュメントによれば多分これでできる。(試してない)
けど、
- アプリ横断的な仕様なのにコントローラーの全アクションメソッドに繰り返し書きたくない
- デフォルトロケールが
"ja"
じゃなくなったら?(何か方法ありそうな気もする。調べてない)
という点でイマイチ。
せめてコントローラーのClassアノテーションでやれたら、全メソッドに書くよりはまだマシかと思って
// src/Controller/HomeController.php
namespace App\Controller;
/**
* @Route("/{_locale}/", name="home_")
* @Route("/", defaults={"_locale" = "ja"})
* @Template()
*/
class HomeController extends Controller
{
/**
* @Route("/", name="index")
*/
public function index()
{
return [];
}
}
とかやってみたけどClassアノテーションでは複数の@Route
は使えなかった。(上が勝つ)
まとめ
かなり強引なハックっぽいやり方だけどコントローラーに一切手を加えずに対応できたのはまあまあ嬉しい。
(なんかもっとスマートな方法を知ってる方や、この方法の問題点等に気づかれた方がいたらぜひ教えていただきたいです!)
追記:ちなみに
URL | 期待する動作 |
---|---|
/ja/ |
日本語でトップページが表示される |
/ja/path/to/page |
日本語でページが表示される |
/en/ |
英語でトップページが表示される |
/en/path/to/page |
英語でページが表示される |
/ |
/ja/ にリダイレクトされる |
/path/to/page |
404 Not Found |
という(普通の?)要件でよければ、
controllers:
resource: ../../src/Controller/
type: annotation
prefix: /{_locale}/
requirements:
_locale: ja|en
default_locale_redirection:
path: /
controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction
defaults:
path: /ja/
permanent: true
これでOK。
Discussion