Keep query-string on every link - php

I want to personalize my Symfony project by letting the user choose a city in a selectbox in the top navi. For that I got a query string e.g. ?city=berlin that I fetch in my controllers and filter the results with.
Is there an easy way to keep that query string on every url alive or would you prefer an other solution without a query string? Maybe with cookies?
Thanks for your help!

Better than talking about cookies is the question about stateful or stateless session. Cookies is just the implementation of mapping the client to the session.
Let's say you have a visitor on one city-parametrized page. What do you except your page to look like, when someone copies the url and shares it with others? city is not any personal state, although you mentioned personalized above (e.g. having responsive pages where I can set the font to 120% size or setting higher contrast, would be a personalized configuration I actually don't want to share in the url).
city is part of the state of the page and not the session, thus we want city to be part of the url. Define a prefix route like /{city} and import another yml with that prefix (http://symfony.com/doc/current/book/routing.html#prefixing-imported-routes).
Every time you generate an url with a city you have to set it. You could do this manually or create some CityDecoratedRouter implements RouterInterface getting #router and #request_stack injected and appends the city parameter to all parameter-arrays in generate() calls.
#EvgeniyKuzmin's answer is imho too much magic no one expects. When dealing with those routes having a city parameter it's better to read it in the code, that the routes are treated differently. Of course you also have to define some new city_path function for twig, which uses our CityDecoratedRouter.

If you need to stick user to some condition base on route path (I will advice you use SEO urls instead of GET query) and then use it as stick filter for some behavior on other pages, then you can do such listener:
BaseKernelEvents:
namespace App\CoreBundle\Listener;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Bundle\FrameworkBundle\Routing\Router;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
abstract class BaseKernelEvents
{
/**
* #var \Symfony\Bundle\FrameworkBundle\Routing\Router
*/
private $router;
/**
* Initializes a new instance of the KernelEvents class.
*/
public function __construct(ContainerInterface $container, Router $router)
{
$this->container = $container;
$this->router = $router;
}
/*
* Store and get value by route param passed to uri to request and session
*/
protected function storeParam(GetResponseEvent $event, $name, $default = 'none')
{
/* #var $request \Symfony\Component\HttpFoundation\Request */
$request = $event->getRequest();
/* #var $session \Symfony\Component\HttpFoundation\Session\Session */
$session = $request->getSession();
// instead of attributes you can get query from request here
$value = $request->attributes->get($name);
if (!$value) {
$value = $session->get($name, $default);
}
return $this->setValues($event, $name, $value);
}
/*
* Set name/value to request context and session
*/
protected function setValues(GetResponseEvent $event, $name, $value)
{
/* #var $request \Symfony\Component\HttpFoundation\Request */
$request = $event->getRequest();
$context = $this->router->getContext();
$session = $request->getSession();
$session->set($name, $value);
$context->setParameter($name, $value);
return $value;
}
}
KernelEvents:
namespace LaMelle\ContentSectionBundle\Listener;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use App\CoreBundle\Listener\BaseKernelEvents;
class KernelEvents extends BaseKernelEvents
{
public function onKernelRequest(GetResponseEvent $event)
{
if (HttpKernelInterface::MASTER_REQUEST === $event->getRequestType())
{
$contentsectionSlug = $this->storeParam($event, 'city');
// DO SOMETHINK BASE ON FILTER
// LIKE CREATE GLOBAL TWIG VAR WITH FILTER VALUE
/* #var \Twig_Environment $globals */
$globals = $this->container->get('twig');
$globals->addGlobal('city', $contentsectionSlug);
}
}
}
So in shown example you will have 'city' be filled from session until you will visit route that change 'city' to other value

Related

Symfony2 set global parameter based on subdomain

using symfony 2.8, i'm working with subdomains and i want to show different (lets say)home pages depending on the subdomain, i'm storing the subdomains in Domain table with a column named subdomain. ideally when the user visits sub.example.com i want to search the database for 'sub' and get the id of that row and set that as a global parameter for that specific domain, so that i can load the websitesettings and load other dynamic data from the database (using domain_id as the key)
this is what i presume to be correct, if there are better methods to deal with this same problem, please let me know, i might get a friend to give out a bounty if its new to me.
I suggest you listen to the kernel.controller event. Make sure your listener is container aware so that you can set the parameter by doing $this->container->setParameter('subdomain', $subdomain);
At this point you just need to check the parameter you set where it suits you, for example in your controller action so that you can return, for example, different views according to the current subdomain.
Reference:
Container aware dispatcher
Symfony2 framework events
Have a look at my implementation, using a YAML configuration instead of a database: https://github.com/fourlabsldn/HostsBundle. You might be able to get some inspiration.
<?php
namespace FourLabs\HostsBundle\Service;
use Symfony\Component\HttpFoundation\RequestStack;
use FourLabs\HostsBundle\Model\DomainRepository;
use FourLabs\HostsBundle\Exception\NotConfiguredException;
abstract class AbstractProvider
{
/**
* #var RequestStack
*/
protected $requestStack;
/**
* #var DomainRepository
*/
protected $domainRepository;
/**
* #var boolean
*/
protected $requestActive;
public function __construct(RequestStack $requestStack, DomainRepository $domainRepository, $requestActive)
{
$this->requestStack = $requestStack;
$this->domainRepository = $domainRepository;
$this->requestActive = $requestActive;
}
protected function getDomainConfig()
{
$request = $this->requestStack->getCurrentRequest();
if(is_null($request) || !$this->requestActive) {
return;
}
$host = parse_url($request->getUri())['host'];
if(!($domain = $this->domainRepository->findByHost($host))) {
throw new NotConfiguredException('Domain configuration for '.$host.' missing');
}
return $domain;
}
}
and the listener
<?php
namespace FourLabs\HostsBundle\EventListener;
use FourLabs\HostsBundle\Service\LocaleProvider;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
class LocaleListener
{
/**
* #var LocaleProvider
*/
private $localeProvider;
public function __construct(LocaleProvider $localeProvider) {
$this->localeProvider = $localeProvider;
}
public function onKernelRequest(GetResponseEvent $event) {
if(HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
return;
}
$event->getRequest()->setLocale($this->localeProvider->getLocale());
}
}

How to make Request type dynamic (validation) on Laravel 5.1

I'm trying make a crud controller base, from where i extend it and set the model base then i have some basic crud methods. I got all working dynamically. But I can't make a dynamic request type, for validate it, i have the ChannelRequest, its working ok as follows but i want it dynamic:
this is my CrudController Class (that I'll extend and set an model):
public function store(ChannelRequest $request)
{
$this->save($request); // this method get the model instantiated in parent class and save the inputs
return redirect('admin/' . $this->plural);
}
in this example above i hardcoded the request type on dependency injection, then it validate, but i want to dynamically change the request type, like this:
// i know it not being work
public function store($this->model .'Request' $request)
{
$this->save($request);
return redirect('admin/' . $this->plural);
}
i tried this:
public function store()
{
$request = new ChannelRequest();
$request->validate(); //hopping it runs like when dependency injection
$this->save($request);
return redirect('admin/' . $this->plural);
}
this throws me to an error:
FatalErrorException in FormRequest.php line 75:
Call to a member function make() on null
in FormRequest.php line 75
at FatalErrorException->__construct() in HandleExceptions.php line 133
at HandleExceptions->fatalExceptionFromError() in HandleExceptions.php line 118
at HandleExceptions->handleShutdown() in HandleExceptions.php line 0
at FormRequest->getValidatorInstance() in ValidatesWhenResolvedTrait.php line 20
at FormRequest->validate() in CrudController.php line 67
First of all I want to stress that having separate controllers for each resource (model) is a good practice and prevents separate concerns from getting too intermixed. Using a dynamic Request class defeats the purpose of explicitly defining a request class in the first place.
However, in the interest of answering the question the best I can I will give you an idea of how to solve this. This code is untested, but the concept should be sound.
What I've done here is extended the standard Request class with a SmartRequest class and overridden the __construct to allow me to run a pre-loader for the proper request class for the given request type.
This will allow you to define the separate request classes, and then load them into a SmartRequest::$subRequest property based on a resourceType request parameter (this can be part of the POST, GET or URL params if you want to modify the code some for the last one).
Code: App\Http\Requests\SmartRequest
<?php
use App\Http\Requests\Request;
class SmartRequest extends Request {
/**
* Holds sub request class
* #var Request
*/
protected $subRequest;
/**
* Default constructor
* #param array $query
* #param array $request
* #param array $attributes
* #param array $cookies
* #param array $files
* #param array $server
* #param string|resource $content
*/
public function __construct(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null)
{
// make sure standard constructor fires
parent::__construct($query, $request, $attributes, $cookies, $files, $server, $content);
// instantiate the sub request object, we must also pass through all the data for the base
// request since the sub class requires this data.
$this->loadSubRequest($query, $request, $attributes, $cookies, $files, $server, $content);
}
/**
* Default constructor
* #param array $query
* #param array $request
* #param array $attributes
* #param array $cookies
* #param array $files
* #param array $server
* #param string|resource $content
*/
public function loadSubRequest($query, $request, $attributes, $cookies, $files, $server, $content)
{
// get resource type off the request data to generate the class string
$class = $this->getRequestClassName();
$this->subRequest = new $class($query, $request, $attributes, $cookies, $files, $server, $content);
}
/**
* Get the sub class name with namespace
* #return string
*/
public function getRequestClass()
{
return '\<path>\<to\<namespace>\\' . studly_case($this->resourceType) . 'Request';
}
/**
* Returns rules based on subclass, otherwise returns default rules
* #return array
*/
public function rules()
{
// return the default rules if we have no sub class
if (empty($this->subRequest)) return [];
// return the rules given by the sub class
return $this->subRequest()->rules();
}
}
Again, this is not real code (as in I have not tested it) but this might be a way to accomplish your request. This also relies on there being some identifier sent on the request (in this case, a requestType parameter), since you won't know anything about request other then where it was sent and with what parameters.
Still, I think this is quite against the intention of this functionality. It is far better to have explicit requests and have them used explicitly in the methods that require them. Why? Self documenting code. People will know what you're using where just by reading things such as ChannelRequest $request in the action. Where something like the above (SmartRequest) will result in some kind of magic happening that any other developer will not understand until tearing open the SmartRequest class.
Let me know if this was confusing, or if you have any other questions about why I think this approach is a step in the wrong direction.

Symfony2 workaround when devices have cookies disabled

For a project I am required to have a persistent session for a visitor.
A couple of years ago I faced the issue with an Apple update temporary rendering all iPhones unable to set PHPSESSID cookies.
I created a fall back method which checked for the SESSION ID in the URL and use that to persist the session between requests. I am aware of the fact this can be enabled in php.ini using the session.use_trans_sid.
Point is I do not want this to happen always. When possible I prefer the cookie method.
Is there a way within Symfony to add this logic to the route methods adding the session identifier?
Can anyone help me to explain where to extend the twig "path" method to add the logic to optionally append the session id to all URL's generated by that method.
UPDATE
Let me post an update on my progress and perhaps someone can help me. I managed to find how to extend the UrlGenerator with my own code by replacing the generator_base_class in a parameter.
Now I have the following issue.
I wish to use a session to do some logic. I however can not reach this core component as a service. I already tried makign a compilerPass for both the UrlGenerator and an extended Router class to be able to make a dependency injection in one of these classes.
However until now it sadly failed.
What would be the best partice to get the Session component within the UrlGenerator class?
I was able to create my solution thanks to this post:
Override router and add parameter to specific routes (before path/url used)
In the end this is the code I came up with.
In my service.xml
<parameters>
<parameter key="router.class">Acme\CoreBundle\Component\Routing\Router</parameter>
<parameter key="router.options.generator_base_class">Acme\CoreBundle\Component\Routing\Generator\UrlGenerator</parameter>
</parameters>
Extending Symfony's core router to make in ContainerAware and force that container to the UrlGenerator.
namespace Acme\CoreBundle\Component\Routing;
use Symfony\Bundle\FrameworkBundle\Routing\Router as BaseRouter;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\RequestContext;
class Router extends BaseRouter implements ContainerAwareInterface
{
private $container;
public function __construct(ContainerInterface $container, $resource, array $options = array(), RequestContext $context = null)
{
parent::__construct($container, $resource, $options, $context);
$this->setContainer($container);
}
public function getGenerator()
{
$generator = parent::getGenerator();
$generator->setContainer($this->container);
return $generator;
}
public function setContainer(ContainerInterface $container = null)
{
$this->container = $container;
}
}
Extending the UrlGenerator class.
namespace Acme\CoreBundle\Component\Routing\Generator;
use Symfony\Component\Routing\Generator\UrlGenerator as BaseUrlGenerator;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* UrlGenerator generates URL based on a set of routes, this class extends the basics from Symfony.
*/
class UrlGenerator extends BaseUrlGenerator implements ContainerAwareInterface
{
private $container;
public function setContainer(ContainerInterface $container = null)
{
$this->container = $container;
}
protected function doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostTokens, array $requiredSchemes = array())
{
/** #var \Symfony\Component\HttpFoundation\Session\Session $session */
$session = $this->container->get('session');
if (true !== $session->get('acceptCookies')) {
$parameters[$session->getName()] = $session->getId();
}
return parent::doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostTokens, $requiredSchemes);
}
}
In the end this results in the session name and id being appended to the generated URL when the session value acceptCookies is not equal to true.

PHPSpec and Laravel - how to handle double method not found issues

I appear to be having issues with my spec tests when it comes to stubs that are calling other methods.
I've been following Laracasts 'hexagonal' approach for my controller to ensure it is only responsible for the HTTP layer.
Controller
<?php
use Apes\Utilities\Connect;
use \OAuth;
class FacebookConnectController extends \BaseController {
/**
* #var $connect
*/
protected $connect;
/**
* Instantiates $connect
*
* #param $connect
*/
function __construct()
{
$this->connect = new Connect($this, OAuth::consumer('Facebook'));
}
/**
* Login user with facebook
*
* #return void
*/
public function initialise() {
// TODO: Actually probably not needed as we'll control
// whether this controller is called via a filter or similar
if(Auth::user()) return Redirect::to('/');
return $this->connect->loginOrCreate(Input::all());
}
/**
* User authenticated, return to main game view
* #return Response
*/
public function facebookConnectSucceeds()
{
return Redirect::to('/');
}
}
So when the route is initialised I construct a new Connect instance and I pass an instance of $this class to my Connect class (to act as a listener) and call the loginOrCreate method.
Apes\Utilities\Connect
<?php
namespace Apes\Utilities;
use Apes\Creators\Account;
use Illuminate\Database\Eloquent\Model;
use \User;
use \Auth;
use \Carbon\Carbon as Carbon;
class Connect
{
/**
* #var $facebookConnect
*/
protected $facebookConnect;
/**
* #var $account
*/
protected $account;
/**
* #var $facebookAuthorizationUri
*/
// protected $facebookAuthorizationUri;
/**
* #var $listener
*/
protected $listener;
public function __construct($listener, $facebookConnect)
{
$this->listener = $listener;
$this->facebookConnect = $facebookConnect;
$this->account = new Account();
}
public function loginOrCreate($input)
{
// Not the focus of this test
if(!isset($input['code'])){
return $this->handleOtherRequests($input);
}
// Trying to stub this method is my main issue
$facebookUserData = $this->getFacebookUserData($input['code']);
$user = User::where('email', '=', $facebookUserData->email)->first();
if(!$user){
// Not the focus of this test
$user = $this->createAccount($facebookUserData);
}
Auth::login($user, true);
// I want to test that this method is called
return $this->listener->facebookConnectSucceeds();
}
public function getFacebookUserData($code)
{
// I can't seem to stub this method because it's making another method call
$token = $this->facebookConnect->requestAccessToken($code);
return (object) json_decode($this->facebookConnect->request( '/me' ), true);
}
// Various other methods not relevant to this question
I've tried to trim this down to focus on the methods under test and my understanding thus far as to what is going wrong.
Connect Spec
<?php
namespace spec\Apes\Utilities;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use \Illuminate\Routing\Controllers\Controller;
use \OAuth;
use \Apes\Creators\Account;
class ConnectSpec extends ObjectBehavior
{
function let(\FacebookConnectController $listener, \OAuth $facebookConnect, \Apes\Creators\Account $account)
{
$this->beConstructedWith($listener, $facebookConnect, $account);
}
function it_should_login_the_user($listener)
{
$input = ['code' => 'afacebooktoken'];
$returnCurrentUser = (object) [
'email' => 'existinguser#domain.tld',
];
$this->getFacebookUserData($input)->willReturn($returnCurrentUser);
$listener->facebookConnectSucceeds()->shouldBeCalled();
$this->loginOrCreate($input);
}
So here's the spec that I'm having issues with. First I pretend that I've got a facebook token already. Then, where things are failing, is that I need to fudge that the getFacebookUserData method will return a sample user that exists in my users table.
However when I run the test I get:
Apes/Utilities/Connect
37 ! it should login the user
method `Double\Artdarek\OAuth\Facade\OAuth\P13::requestAccessToken()` not found.
I had hoped that 'willReturn' would just ignore whatever was happening in the getFacebookUserData method as I'm testing that separately, but it seems not.
Any recommendations on what I should be doing?
Do I need to pull all of the OAuth class methods into their own class or something? It seems strange to me that I might need to do that considering OAuth is already its own class. Is there some way to stub the method in getFacebookUserData?
Update 1
So I tried stubbing the method that's being called inside getFacebookUserData and my updated spec looks like this:
function it_should_login_the_user($listener, $facebookConnect)
{
$returnCurrentUser = (object) [
'email' => 'existinguser#domain.tld',
];
$input = ['code' => 'afacebooktoken'];
// Try stubbing any methods that are called in getFacebookUserData
$facebookConnect->requestAccessToken($input)->willReturn('alongstring');
$facebookConnect->request($input)->willReturn($returnCurrentUser);
$this->getFacebookUserData($input)->willReturn($returnCurrentUser);
$listener->facebookConnectSucceeds()->shouldBeCalled();
$this->loginOrCreate($input);
}
The spec still fails but the error has changed:
Apes/Utilities/Connect
37 ! it should login the user
method `Double\Artdarek\OAuth\Facade\OAuth\P13::requestAccessToken()` is not defined.
Interestingly if I place these new stubs after the $this->getFacebookUserData stub then the error is 'not found' instead of 'not defined'. Clearly I don't fully understand the inner workings at hand :D
Not everything, called methods in your dependencies have to be mocked, because they will in fact be called while testing your classes:
...
$facebookConnect->requestAccessToken($input)->willReturn(<whatever it should return>);
$this->getFacebookUserData($input)->willReturn($returnCurrentUser);
...
If you don't mock them, phpspec will raise a not found.
I'm not familiar with the classes involved but that error implies there is not method Oauth:: requestAccessToken().
Prophecy will not let you stub non-existent methods.

Adding Default Variables to Zend2 ViewModel

What's the "Zend" way of adding default variables to the ViewModel.
Currently I have:
return new ViewModel(array('form' => new CreateUserForm));
But I want to always add some variables to the ViewModel array. Like the time and date say, or categories for a menu. I was thinking of extending the ViewModel as that seems the OO way, but Zend always does things differently...
You could always extend the ViewModel if you want some extra functionality in there...
class MyViewModel extends ViewModel
{
/**
* Default Variables to set
*/
protected $_defaultValues = array(
'test' => 'bob'
);
/**
* Constructor
*
* #param null|array|Traversable $variables
* #param array|Traversable $options
*/
public function __construct($variables = null, $options = null)
{
//$variables = array_merge($this->_defaultValues, $variables);
$this->setVariables($this->_defaultValues);
parent::__construct($variables, $options)
}
}
Now in your controller just use return your new view model instead:
/**
* Some Controller Action
*/
function myAction()
{
return new MyViewModel();
}
One approach could be to have a method in your controller that returns ViewModel populated with time, date, etc. and then addVariables() to the returned model in the Action.
However, a better approach will be to use view helpers since they will be available in every view/layout throughout the application.

Categories