I have a Symfony project that uses two different domains for two different countries. The site is a client login interface and is basically the same for both domains except for branding and minor name differences. That part is okay.
The two domains are defined in my parameters as us_domain and ca_domain, and in my routing.yml I have:
clients:
resource: #ClientsBundle/Resources/config/routing.yml
host: "clients.{domain}"
prefix: /
requirements:
domain: %us_domain%|%ca_domain%
defaults: { domain: "%us_domain%" }
In Twig, I have my menu using:
<li><span>Home</span></li>
The problem is that although the page will come up on either domain, the paths being generated always use the us_domain, apparently pulling it from defaults in my routing. (I can switch this to ca_domain and the paths do switch).
My question is, why isn't the current domain being detected and used? It seems like the default should be overridden by whatever domain is actually being used?
I'm running Nginx if that matters.
I somehow had missed that the routing variables weren't going to be auto-detected, but you had to pass them as part of your calls. I ended up solving this with a custom Twig filter.
Where I used my path, I passed the domain variable pulling from app.request.host, such as
<li><span>Home</span></li>
And then just wrote Twig extension that parsed/evaluated the host and passed back the domain
<?php
namespace AppBundle\Twig;
use JMS\DiExtraBundle\Annotation as DI;
/**
* Class TwigUrlExtension
* #DI\Service("twig.extension.url")
* #DI\Tag("twig.extension")
*
*/
class TwigUrlExtension extends \Twig_Extension
{
protected $ca_domain;
protected $us_domain;
/**
*
* #DI\InjectParams({
* "us_domain" = #DI\Inject("%us_domain%"),
* "ca_domain" = #DI\Inject("%ca_domain%")
* })
* #param $us_domain
* #param $ca_domain
*/
public function __construct($us_domain, $ca_domain)
{
$this->us_domain = $us_domain;
$this->ca_domain = $ca_domain;
}
/**
* {#inheritdoc}
*/
public function getFunctions() {
return array(
'domainFromHost' => new \Twig_Function_Method($this, 'toDomain')
);
}
/**
* #param string $string
* #return int
*/
public function toDomain ($string) {
if(stripos($string,$this->ca_domain) !== false)
return $this->ca_domain;
if(stripos($string,$this->us_domain) !== false)
return $this->us_domain;
return $string;
}
/**
* {#inheritdoc}
*/
public function getName() {
return 'twig_url_extension';
}
}
Related
I have a ArgumentValueResolverInterface that creates and validates DTOs.
I have also setup a firewall to protect routes and additionally use IsGranted attribute for fine grained access control.
Problem is that the value resolver and validation runs before the security firewall and show validation errors even if the request is unauthenticated.
How can I change the value resolver to run after security is resolved?
Is this even possible?
class RequestDTOValueResolver implements ArgumentValueResolverInterface
{
/**
* RequestDTOValueResolver constructor.
* #param ValidatorInterface $validator
*/
public function __construct(protected ValidatorInterface $validator)
{}
/**
* #inheritDoc
*/
public function supports(Request $request, ArgumentMetadata $argument): bool
{
return is_subclass_of($argument->getType(), RequestDTOInterface::class);
}
/**
* #inheritDoc
* #throws ValidationException
* #throws Exception
*/
public function resolve(Request $request, ArgumentMetadata $argument): iterable
{
$className = $argument->getType();
/** #var AbstractRequestDTO $dto */
$dto = new $className($request); //$this->parseRequest($request, $argument);
$groups = $dto->getGroups();
$errors = $this->validator->validate($dto, null, !empty($groups) ? $groups : null);
if ($errors->count()) {
throw ValidationException::create($errors, "One or more fields are invalid.");
}
yield $dto;
}
}
According to the official documentation, which is available here (it's not so different across different SF versions) : https://symfony.com/doc/5.2/controller/argument_value_resolver.html
You could probably achieve your goal by setting the proper priority
App\ArgumentResolver\UserValueResolver:
tags:
- { name: controller.argument_value_resolver, priority: 50 }
I would also advice to check in which order each service is being run. Here you can see how it's done by SF:
https://symfony.com/doc/current/introduction/http_fundamentals.html
I have a controller which handles a GET request. I need to set requirement parameters for GET request, e.g.: 'http://localhost/site/main?id=10&sort=asc
My controller class
class IndexController extends Controller {
` /**
* #Route
* (
* "/site/main",
* name="main"
* )
*
* #Method("GET")
*/
public function mainAction(Request $request)
{
return new Response('', 200);
}
}
How could I do that?
UPD: I need to set requirement for URL parameters like
id: "\d+",
sort: "\w+"
Etc.
The same as symfony allows to do with POST request.
You can specify the requirements in the "#Route" annotation like this:
class IndexController extends Controller {
` /**
* #Route
* (
* "/site/main",
* name="main",
* requirements={
* "id": "\d+",
* "sort": "\w+"
* })
* )
*
* #Method("GET")
*/
public function mainAction(Request $request)
{
return new Response('', 200);
}
}
#Method is what you need http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/routing.html#route-method
If you try to use this route with POST, you will have 404
I couldn't understand your question well.
However, if what you need is to set up a filter mechanism for the GET method parameters as it is already available for the URL using route requirements, I think there is no ready to use tools for this in the Route component, as commented #Yoshi.
I had to do this kind of work myself and used this. I hope it helps you too
public function indexAction(Request $request)
{
// Parameter names used in the current request
$current_request_params=array_keys($request->query->all());
// $ALLOWED_INDEX_PARAMS should be declared as Class static property array and hold names of the query parameters you want to allow for this method/action
$unallowed_request_params=array_diff($current_request_params,PersonController::$ALLOWED_INDEX_PARAMS);
if (!empty($unallowed_request_params))
{
$result=array("error"=>sprintf("Unknown parameters: %s. PLease check the API documentation for more details.",implode($unallowed_request_params,", ")));
$jsonRsp=$this->get("serializer")->serialize($result,"json");
return new Response($jsonRsp,Response::HTTP_BAD_REQUEST,array("Content-Type"=>"application/json"));
}
// We are sure all parameters are correct, process the query job ..
}
I'm trying to show only selected fields in my REST action in controller.
I've found one solution - I can set groups in Entities/Models and select this group in annotation above action in my Controller.
But actually i don't want use groups, i want determine which fields i wanna expose.
I see one solution - I can create one group for every field in my Entities/Model. Like this:
class User
{
/**
* #var integer
*
* #Groups({"entity_user_id"})
*/
protected $id;
/**
* #var string
*
* #Groups({"entity_user_firstName"})
*/
protected $firstName;
/**
* #var string
*
* #Groups({"entity_user_lastName"})
*/
protected $lastName;
}
And then i can list fields above controller action.
My questions are:
Can I use better solution for this?
Can I list all groups? Like I can list all routes or all services.
This is mainly about serialization not about fosrestbundle itself.
The right way would be to create your own fieldserialization strategy.
This article got it down really nicely:
http://jolicode.com/blog/how-to-implement-your-own-fields-inclusion-rules-with-jms-serializer
It build a custom exclusion strategy as describeted here:
How do I create a custom exclusion strategy for JMS Serializer that allows me to make run-time decisions about whether to include a particular field?
Example code from first link for reference:
custom FieldExclusion strategy:
namespace Acme\Bundle\ApiBundle\Serializer\Exclusion;
use JMS\Serializer\Exclusion\ExclusionStrategyInterface;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Context;
class FieldsListExclusionStrategy implements ExclusionStrategyInterface
{
private $fields = array();
public function __construct(array $fields)
{
$this->fields = $fields;
}
/**
* {#inheritDoc}
*/
public function shouldSkipClass(ClassMetadata $metadata, Context $navigatorContext)
{
return false;
}
/**
* {#inheritDoc}
*/
public function shouldSkipProperty(PropertyMetadata $property, Context $navigatorContext)
{
if (empty($this->fields)) {
return false;
}
$name = $property->serializedName ?: $property->name;
return !in_array($name, $this->fields);
}
}
Interface
interface ExclusionStrategyInterface
{
public function shouldSkipClass(ClassMetadata $metadata, Context $context);
public function shouldSkipProperty(PropertyMetadata $property, Context $context);
}
usage
in controller or where you need it:
$context = new SerializationContext();
$fieldList = ['id', 'title']; // fields to return
$context->addExclusionStrategy(
new FieldsListExclusionStrategy($fieldList)
);
// serialization
$serializer->serialize(new Pony(), 'json', $context);
You should be also able to mix and match with groups eg. you can also set $content->setGroups(['myGroup']) together with the fieldExclusio
I'm attempting to learn how to use the Symfony 2.3 framework. I thought it would be a good first exercise to modify Acme\DemoBundle\DemoController::helloaction() to provide a default name when none was entered.
This is the original:
/**
* #Route("/hello/{name}", name="_demo_hello")
* #Template()
*/
public function helloAction($name)
{
return array('name' => $name);
}
It works with urls like localhost/Symfony/web/demo/hello/SOMENAME and fails with urls like localhost/Symfony/web/demo/hello/SOMENAME/, localhost/Symfony/web/demo/hello and localhost/Symfony/web/demo/hello/
This is what I did:
/**
* #Route("/hello", name="_demo_hello", defaults={"name" = "World"})
* #Template()
*/
public function helloAction($name)
{
return array('name' => $name);
}
It works with localhost/Symfony/web/demo/hello and fails with localhost/Symfony/web/demo/hello/SOMENAME, localhost/Symfony/web/demo/hello/SOMENAME/ and localhost/Symfony/web/demo/hello/
How do I make the routing work with and without a name and with and without a trailing slash?
You can set a default value like this:
/**
* #Route("/hello/", defaults={"name" = "John"})
* #Route("/hello/{name}", name="_demo_hello")
* #Template()
*/
public function helloAction($name) { ... }
It's also important to know that you can have more than one route on the same action, so no need to duplicate actions.
See documentation: http://symfony.com/doc/2.2/book/controller.html And: #Route Documentation
I think your solution should also work if you append a / after your route /hello.
File based translations don't work for me because clients need to change the texts.
So I am thinking about implementing this interface to fetch data from the database and cache the results in an APC cache.
Is this a good solution?
This could be what you are looking for:
Use a database as a translation provider in Symfony 2
Introduction
This article explain how to use a database as translation storage in Symfony 2. Using a database to provide translations is quite easy to do in Symfony 2, but unfortunately it’s actually not explained in Symfony 2 website.
Creating language entities
At first, we have to create database entities for language management. In my case, I’ve created three entities : the Language entity contain every available languages (like french, english, german).
The second entity is named LanguageToken. It represent every available language tokens. The token entity represent the source tag of the xliff files. Every translatable text available is a token. For example, I use home_page as a token and it’s translated as Page principale in french and as Home page in english.
The last entity is the LanguageTranslation entity : it contain the translation of a token in a specific language. In the example below, the Page principale is a LanguageTranslation entity for the language french and the token home_page.
It’s quite inefficient, but the translations are cached in a file by Symfony 2, finally it’s used only one time at Symfony 2 first execution (except if you delete Symfony 2’s cache files).
The code of the Language entity is visible here :
/**
* #ORM\Entity(repositoryClass="YourApp\YourBundle\Repository\LanguageRepository")
*/
class Language {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue
*/
private $id;
/** #ORM\column(type="string", length=200) */
private $locale;
/** #ORM\column(type="string", length=200) */
private $name;
public function getId() {
return $this->id;
}
public function setId($id) {
$this->id = $id;
}
public function getLocale() {
return $this->locale;
}
public function setLocale($locale) {
$this->locale = $locale;
}
public function getName() {
return $this->name;
}
public function setName($name) {
$this->name = $name;
}
}
The code of the LanguageToken entity is visible here :
/**
* #ORM\Entity(repositoryClass="YourApp\YourBundle\Repository\LanguageTokenRepository")
*/
class LanguageToken {
/**
* #ORM\Id #ORM\Column(type="integer")
* #ORM\GeneratedValue
*/
private $id;
/** #ORM\column(type="string", length=200, unique=true) */
private $token;
public function getId() {
return $this->id;
}
public function setId($id) {
$this->id = $id;
}
public function getToken() {
return $this->token;
}
public function setToken($token) {
$this->token = $token;
}
}
And the LanguageTranslation entity’s code is visible here :
/**
* #ORM\Entity(repositoryClass="YourApp\YourBundle\Repository\LanguageTranslationRepository")
*/
class LanguageTranslation {
/**
* #ORM\Id #ORM\Column(type="integer")
* #ORM\GeneratedValue
*/
private $id;
/** #ORM\column(type="string", length=200) */
private $catalogue;
/** #ORM\column(type="text") */
private $translation;
/**
* #ORM\ManyToOne(targetEntity="YourApp\YourBundle\Entity\Language", fetch="EAGER")
*/
private $language;
/**
* #ORM\ManyToOne(targetEntity="YourApp\YourBundle\Entity\LanguageToken", fetch="EAGER")
*/
private $languageToken;
public function getId() {
return $this->id;
}
public function setId($id) {
$this->id = $id;
}
public function getCatalogue() {
return $this->catalogue;
}
public function setCatalogue($catalogue) {
$this->catalogue = $catalogue;
}
public function getTranslation() {
return $this->translation;
}
public function setTranslation($translation) {
$this->translation = $translation;
}
public function getLanguage() {
return $this->language;
}
public function setLanguage($language) {
$this->language = $language;
}
public function getLanguageToken() {
return $this->languageToken;
}
public function setLanguageToken($languageToken) {
$this->languageToken = $languageToken;
}
}
Implementing a LoaderInterface
The second step is to create a class implementing the Symfony\Component\Translation\Loader\LoaderInterface. The corresponding class is shown here :
class DBLoader implements LoaderInterface{
private $transaltionRepository;
private $languageRepository;
/**
* #param EntityManager $entityManager
*/
public function __construct(EntityManager $entityManager){
$this->transaltionRepository = $entityManager->getRepository("AppCommonBundle:LanguageTranslation");
$this->languageRepository = $entityManager->getRepository("AppCommonBundle:Language");
}
function load($resource, $locale, $domain = 'messages'){
//Load on the db for the specified local
$language = $this->languageRepository->getLanguage($locale);
$translations = $this->transaltionRepository->getTranslations($language, $domain);
$catalogue = new MessageCatalogue($locale);
/**#var $translation Frtrains\CommonbBundle\Entity\LanguageTranslation */
foreach($translations as $translation){
$catalogue->set($translation->getLanguageToken()->getToken(), $translation->getTranslation(), $domain);
}
return $catalogue;
}
}
The DBLoader class need to have every translations from the LanguageTranslationRepository (the translationRepository member). The getTranslations($language, $domain) method of the translationRepository object is visible here :
class LanguageTranslationRepository extends EntityRepository {
/**
* Return all translations for specified token
* #param type $token
* #param type $domain
*/
public function getTranslations($language, $catalogue = "messages"){
$query = $this->getEntityManager()->createQuery("SELECT t FROM AppCommonBundle:LanguageTranslation t WHERE t.language = :language AND t.catalogue = :catalogue");
$query->setParameter("language", $language);
$query->setParameter("catalogue", $catalogue);
return $query->getResult();
}
...
}
The DBLoader class will be created by Symfony as a service, receiving an EntityManager as constructor argument. All arguments of the load method let you customize the way the translation loader interface work.
Create a Symfony service with DBLoader
The third step is to create a service using the previously created class. The code to add to the config.yml file is here :
services:
translation.loader.db:
class: MyApp\CommonBundle\Services\DBLoader
arguments: [#doctrine.orm.entity_manager]
tags:
- { name: translation.loader, alias: db}
The transation.loader tag indicate to Symfony to use this translation loader for the db alias.
Create fake translation files
The last step is to create an app/Resources/translations/messages.xx.db file for every translation (with xx = en, fr, de, …).
I didn’t found the way to notify Symfony to use DBLoader as default translation loader. The only quick hack I’ve found is to create a app/Resources/translations/messages.en.db file. The db extension correspond to the db alias used in the service declaration. A corresponding file is created for every language available on the website, like messages.fr.db for french or messages.de.db for german.
When Symfony find the messages.xx.db file he load the translation.loader.db to manage this unknown extension and then the DBLoader use database content to provide translation.
I’ve also didn’t found the way to clean properly the translations cache on database modification (the cache have to be cleaned to force Symfony to recreate it). The code I actually use is visible here :
/**
* Remove language in every cache directories
*/
private function clearLanguageCache(){
$cacheDir = __DIR__ . "/../../../../app/cache";
$finder = new \Symfony\Component\Finder\Finder();
//TODO quick hack...
$finder->in(array($cacheDir . "/dev/translations", $cacheDir . "/prod/translations"))->files();
foreach($finder as $file){
unlink($file->getRealpath());
}
}
This solution isn’t the pretiest one (I will update this post if I find better solution) but it’s working ^^
Be Sociable, Share!
Take a look at the Translatable behavior extension for Doctrine 2. StofDoctrineExtensionsBundle integrates it with Symfony.
You may want to take a look into this Loader + Resource using PDO connection: https://gist.github.com/3315472
You then only need to make it cache aware, like adding a memcache, apc, .. in between.
If so, you can then disable the filecaching of the Translator itself.