I have a symfony2 web project consisting of ten pages which are rendered via 5 controllers. User can land a site through any of the pages above (via shared link for example). And I need to show a welcome popup (just div with position:absolute) to users who opens the page for the first time during the current session.
I've already placed my popup in the common twig template which is used by all the pages needed. Now I have to determine whether to show popup or not. I'm going to show popup based on boolean value from controller.
I have to work with session and cookies, but I have to do that on each page and I don't want to write the same code (check and set cookies, output a boolean to show popup in template) in every method of each controller. Is there a way to this according to DRY concepts?
You could make a wrapper class which handles checking, setting, and getting the current session values and make it a service.
<?php
namespace My\Bundle\My\Namespace;
use Symfony\Component\HttpFoundation\Session\Session;
class SessionManager /* or whatever you want to call it */
{
public function __construct(Session $session)
{
$this->session = $session;
}
public function getMyValue()
{
return $this->session->get('my.value.key',null);
}
public function setMyValue($value)
{
$this->session->set('my.value.key',$value);
return $this;
}
public function hasMyValue()
{
return $this->session->has('my.value.key');
}
}
And in your bundle services.yml
<service id="mytag.session_manager" class="My\Bundle\My\Namespace\SesionManager">
<argument type="service" id="session" />
</service>
And in your controllers
public function someAction()
{
$sessionManager = $this->get('mytag.session_manager');
if($sessionManager->hasMyValue())
{
// do something
}
}
Thanks to the Sgoettschkes answer here Where to place common business logic for all pages in symfony2
I tried this method
http://symfony.com/doc/current/book/templating.html#embedding-controllers
And it looks awesome:
My Popup is included in the main template like this
{{ render(controller('MalyutkaMainBundle:WelcomePopup:index')) }}
Than inside the controller I manipulate session vars
class WelcomePopupController extends Controller {
public function indexAction(Request $request) {
$session = $this->get('session');
$showWelcomePopup = 0;
if ($session->has("have_seen_welcome_popup_on")) {
// tbd compare to the date of publishing of the new popup screen
} else {
$showWelcomePopup = 1;
$session->set("have_seen_welcome_popup_on", new \DateTime());
}
$params = array (
'show_welcome_popup' => $showWelcomePopup
);
return $this->render('MalyutkaMainBundle:WelcomePopup:welcome_popup.html.twig', $params);
}
}
And nothing is to be added in other controllers - just what I wanted to do.
But it is impossible to change cookies that way, so I store my data in the session.
Related
I have an entity BlogPost with a status property. This status property depends on an external API call which is handled via the doctrine postLoad event. All other properties are stored in the local database.
public function postLoad(BlogPost $post)
{
$this->postHandler->calculateStatus($post);
}
The problem is, in some cases i don't want to calculate the status at all. For example if i want to get only the description of all blogposts.
With the code above, all blog entities being loaded will trigger the postLoad event even if i just want to have values from a local database. That is very expensive and not acceptable.
So for example in my repository class i want to get all BlogPosts having a website without invoking the postLoad event.
public function findBlogPosts()
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('bp')
->from('AppBundle:BlogPosts', 'bp')
->innerJoin('bp.website', 'w');
return $qb->getQuery()->getResult();
}
Is there a way to say "Yes, load the BlogPost collection, but do not fire event!" ???
Any other approaches? Custom event?
Thanks
Why don't just move this logic outside the post entity and event listener? If you know when you need to calculate the status you can do it explicitly.
For example
$post = $this->entityManager->find(BlogPost::class, $postId);
$status = $this->postHandler->calculateStatus($post);
The other approach I could suggest is not good but works. You could use lazy calculation and instead of calling $this->postHandler->calculateStatus($this) in postLoad event listener you could inject postHandler service into entity and perform the calculation in the moment you actually need it.
For example if you need calculation when calling $blogPost->getStatus() method, you could do it this way:
interface PostHandlerAwareInterface
{
public function setPostHandler(PostHandlerInterface $postHandler): void;
}
class EntityServiceInjectorEventSubscriber implements EventSubscriber
{
/** #var PostHandlerInterface */
private $postHandler;
public function postLoad($entity): void
{
$this->injectServices($entity);
}
public function postPersist($entity): void
{
$this->injectServices($entity);
}
private function injectServices($entity): void
{
if ($entity instanceof PostHandlerAwareInterface) {
$entity->setPostHandler($this->postHandler);
}
}
}
class BlogPost extends PostHandlerAwareInterface
{
/** #var PostHandlerInterface */
private $postHandler;
private $status;
public function setPostHandler(PostHandlerInterface $postHandler): void
{
$this->postHandler = $postHandler;
}
public function getStatus()
{
if (null === $this->status) {
$this->postHandler->calculateStatus($this);
}
return $this->status;
}
}
If you don't like this idea you still could manage it via (BUT I STRONGLY DO NOT RECOMMEND DO THIS DIRTY HACK) setting the flag to your entity event listener.
You could inject your entity event listener to the code and set flag before fetching data:
class BlogPostCalculateStatusListener
{
/** #var bool */
private $calculationEnabled = true;
public function suspendCalculation(): void
{
$this->calculationEnabled = false;
}
public function resumeCalculation(): void
{
$this->calculationEnabled = true;
}
public function postLoad(BlogPost $post): void
{
if ($this->calculationEnabled) {
$this->postHandler->calculateStatus($post);
}
}
}
$this->calculateStatusListener->suspendCalculation();
$blogPosts = $blogPostRepository->findBlogPosts();
$this->calculateStatusListener->resumeCalculation();
Hope this helps.
PS. If you want to get only the descriptions of all blog posts you can do this way:
class BlogPostRepository
{
public function findBlogPosts()
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('bp.description')
->from('AppBundle:BlogPosts', 'bp')
->innerJoin('bp.website', 'w');
return $qb->getQuery()->getArrayResult();
}
}
getArrayResult does not invoke lifecycle callbacks.
Since i haven't found a real similar use case on the internet, i'll go for the following solution which seems the easiest and most acceptable cleanest to me. Maybe someone else could find this useful.
Implement a TransientLoadable Interface
interface TransientLoadable
{
public function isLoaded() : bool;
public function setLoaded(bool $loaded) : TransientLoadable;
public function setTransientLoadingFunction(\Closure $loadingFunction) :
TransientLoadable;
}
Implement the entity
class BlogPost implements TransientLoadable
{
...
}
Setup Loading function on postLoad Event
public function postLoad(BlogPost $post)
{
$func = function() use ($postHandler, $post)
{
//Since there may be another fields being loaded from the same API, catch them also since data is anyway in the same request
$postHandler->setAllDataFromAPI($post)
//Set the loading state to true to prevent calling the API again for the next property which may also be transient
$post->setLoaded(true);
}
$post->setTransientLoadingFunction($func)
}
Use the built-in lazy loading mechanism to get the property from the API only when it's needed
class BlogPost implements TransientLoadable
{
private function getStatus() : int
{
if (!$this->isLoaded) {
call_user_function($this->loadingFunction)
}
return $this->status;
}
private function getVisitorCount() : int
{
if (!$this->isLoaded) {
call_user_function($this->loadingFunction)
}
return $this->visitorCount;
}
}
So what's happening? Let's imagine we want to get the status and the visitor count, both are loaded via a single external API call.
If some api-dependent property of the entity is needed, all other properties gets loaded too (since we don't want to have for each property another call). This in ensured through the loaded function of the TransientLoadable interface. All data gets loaded by the setAllDataFromAPI function which is injected as a closure function.
I think that is not the cleanest solution. The loading stuf should be done by an extra layer on top of the entity class. Since sonata admin does not deal with such an layer, i think that this solution is cleaner than writing the loading mechanism directly to the entity class.
I am open to another suggestions or feedback
Thanks
My Controller has a Factory that gives it a Form
$formManager = $container->get('FormElementManager');
return new MyController(
$formManager->get(MyForm::class)
);
My Form has also a Factory that gives it an AuthenticationService
return new MyForm(
$container->get(AuthenticationService::class)
);
That way I can check in the form if the user has identity.
But how can i redirect him from the form?
Just like in a Controller?
if(!$authService->hasIdentity()) {
return $this->redirect()->toRoute('myRoute);
}
Or how can i redirect from a (Controller and/or Form) Factory?
A possible solution for your issue could be the possibilty of using the build method with the factory call.
You haven 't shown your factories, so I will use some standard examples, which explain the solution.
The first approach is not injecting the whole form to the controller. Instead just inject the form element manager. So you can use the build method of the factory inside your controller.
The controller factory
namespace Application\Controller\Factory;
use Application\Controller\YourController;
use Interop\Container\ContainerInterface;
use Zend\ServiceManager\Factory\FactoryInterface;
class YourControllerFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$formElementManager = $container->get('FormElementManager');
return new YourController($formElementManager);
}
}
This differs from your original factory. Only the form element manager is injected to the controller. This holds a few advantages for you. One of this is the build method of the manager.
The Controller
namespace Application\Controller;
class YourController extends AbstractActionController
{
protected $formElementManager;
public function __construct($formElementManager)
{
$this->formElementManager = $formElementManager;
}
public function indexAction()
{
$user = $this->currentUser();
if ($user === null) {
$this->redirect('to/somewhere/the/user/belongs');
}
// here 's the magic!
$form = $this->formElementManager->build(YourForm::class, [
'userID' => $user->getUserId(),
]);
// some form stuff follows here
}
}
As the form was not injected directly to your controller but the form element manager, you can use the form element manager instead inside the controller. This offers you the opportunity to use the build function. With this function you can add some options to your form factory. In this case I 'm using the user id for the form factory.
If there 's no valid user, no form will be created because an exception is thrown before.
The Form Factory
The form factory creates a new instance of your form. All needed dependencies should be created in the factory. How the build function works here, I 'll explain later in the answer.
namespace Application\Form\Factory;
use Application\Form\YourForm;
use Interop\Container\ContainerInterface;
use Zend\ServiceManager\Factory\FactoryInterface;
class YourFormFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$selectOptions = [];
if ($options !== null) {
if (isset($options['userID])) {
$tablegateway = $container->get(YourTableGateway::class);
$selectOptions = $tablegateway->findOptionsByUserId($options['userID]);
}
}
$form = $container->get(YourForm::class);
if (count($selectOptions))
$form->get('YourSelectElement')->setValueOptions($selectOptions);
return $form;
}
}
This factory does all you need. Via the build method you hand over the user id. If a user id is present a table gateway is created from wich you retrieve select options by the given user id. These options will be set to the form field. This logic is kept in the factory to keep the form class itself clean and simple.
With this solution you don 't need the auth service in your form. Your form is only generated when a valid user id is given. Your form instance will not crash, if there 's no user id given. The only conceivable case could be a form with default or no select options for the specific field.
Hope this helps a bit.
supposed having certain route string like "/path/index.html" protected by firewall, how to chek whether current user is able to access it?
Thanks in advance!
I am sorry, I should have been more explicit: I have an array of route names and I construct a menu. A lot of users with different roles can access a page with this menu. The purpose is to show only accessible liks in this menu for a particular user.
Something like:
'security_context'->'user'->isGranted('/path/index.html')
This answer is based on your comments:
You should get the roles needed to access that route.to that you need access to the security.access_map service which is private.so it has to be injected directly.e.g: you can create a path_roles service like such that you can get the roles for a certain path:
namespace Acme\FooBundle;
class PathRoles
{
protected $accessMap;
public function __construct($accessMap)
{
$this->accessMap = $accessMap;
}
public function getRoles($path)
{ //$path is the path you want to check access to
//build a request based on path to check access
$request = Symfony\Component\HttpFoundation\Request::create($path, 'GET');
list($roles, $channel) = $this->accessMap->getPatterns($request);//get access_control for this request
return $roles;
}
}
now declare it as a service:
services:
path_roles:
class: 'Acme\FooBundle\PathRoles'
arguments: ['#security.access_map']
now use that service in your controller to get the roles for the path and construct your menu based on those roles and isGranted.i.e:
//code from controller
public function showAction(){
//do stuff and get the link path for the menu,store it in $paths
$finalPaths=array();
foreach($paths as $path){
$roles = $this->get('path_roles')->getRoles($path);
foreach($roles as $role){
$role = $role->getRole();//not sure if this is needed
if($this->get('security.context')->isGranted($role)){
$finalPaths[] = $path;
break;
}
}
//now construct your menu based on $finalPaths
}
}
You could use security.access_control configuration option:
securty:
access_control:
- { path: "^/path/index.html$", roles: ROLE_SOME_ROLE}
Or simply check that manually from within your controller:
class SomeController extends Controller {
public function indexAction() {
if (!$this->get('security.context')->isGranted(...)) {
throw new AccessDeniedException(...);
}
...
}
}
I want to create a Twig extension and use this:
{{ new_func(route-name) }}
To do the same thing as:
{{ render_esi(url(route-name)) }}
...but with some adjustments
It's nearly done but it's this line that needs to be changed, but I can't see how I can call an ESI from this code (outside of Twig):
return $environment->render($route); /// needs to receive route and render an ESI
-
namespace Acme\Bundle\MyBundle\Twig;
class NewTwigFunction extends \Twig_Extension
{
private $request;
public function __construct($container)
{
$this->request = $container->get('request');
}
public function getFunctions() {
return array(
'new_func' => new \Twig_Function_Method($this, 'newFunction', array('needs_environment' => true) )
);
}
public function newFunction(\Twig_Environment $environment, $route) {
$r = $this->request;
return $environment->render($route);
}
public function getName() {
return "new_func";
}
}
I'm not sure I follow why would you need this, but I think it's great as an example of an abstract question:
How do I track down & extend core functionality of Symfony2?
Finding the functionality
Seems that you're having trouble finding where is this render_esi executed, so let's tackle that!
This doesn't seem like a standard Twig feature, so it must be an extension, just like the one you're creating.
It should be located somewhere in Symfony2 core files, so we start looking into vendor/symfony/src folder. Since we already know that we're dealing with an extension of Twig, Component folder is out of the question (because Twig is a separate library from Symfony2 core components).
So we've narrowed it down to Bridge and Bundle. If we look inside them then we see Bundle/TwigBundle or Bridge/Twig. We also know that Symfony2 developers follow a strict code/architecture style, so we know exactly which folder to look for - Extension. Now it's just a matter of checking them both.
Long story short we find what we're looking for in vendor/symfony/src/Symfony/Bridge/Twig/Extension/HttpKernelExtension, where we see a render_* function. Jackpot!
Extending the functionality
Before changing anything, we need to first emulate what's already there, so we create something like this:
use Symfony\Component\HttpKernel\Fragment\FragmentHandler;
class NewTwigFunction extends \Twig_Extension
{
private $handler;
public function __construct(FragmentHandler $handler)
{
$this->handler = $handler;
}
public function getFunctions()
{
return array(
'new_func' => new \Twig_Function_Method($this, 'newFunction', array('is_safe' => array('html')) )
);
}
public function newFunction($uri, $options = array())
{
return $this->handler->render($uri, 'esi', $options);
}
public function getName()
{
return "new_func";
}
}
Now when you call
{{ new_func(url(route-name)) }}
you should see same results as
{{ render_esi(url(route-name)) }}
But we still need to get rid of the url part.
Easy as pie, we just add the router service to our extension! Now our extension could look like this:
use Symfony\Component\Routing\Router;
use Symfony\Component\HttpKernel\Fragment\FragmentHandler;
class NewTwigFunction extends \Twig_Extension
{
private $handler;
private $router;
public function __construct(FragmentHandler $handler, Router $router)
{
$this->handler = $handler;
$this->router = $router;
}
public function getFunctions()
{
return array(
'new_func' => new \Twig_Function_Method($this, 'newFunction', array('is_safe' => array('html')) )
);
}
public function newFunction($routeName, $options = array())
{
$uri = $this->router->generate($routeName);
return $this->handler->render($uri, 'esi', $options);
}
public function getName()
{
return "new_func";
}
}
and {{ new_func(route-name) }} should work as expected.
Hooking in-between
The way I understood it, you want almost the same functionality as render_esi, but with slight changes to output.
So that means that we need to hook somewhere in-between return and $this->handler->render($uri, $strategy, $options);.
How deep down the rabbit hole we need to go depends on the change.
For example, if you want to alter Response object before it's turned into actual html string, you need to find the spot where it's turned in the first place. A good bet would be to look into FragmentHandler:
protected function deliver(Response $response)
{
if (!$response->isSuccessful()) {
throw new \RuntimeException(sprintf('Error when rendering "%s" (Status code is %s).', $this->request->getUri(), $response->getStatusCode()));
}
if (!$response instanceof StreamedResponse) {
return $response->getContent();
}
$response->sendContent();
}
Got it! Now you just need to extend FragmentHandler::deliver() and pass your implementation of it into your twig extenion.
Tracking down configuration
You have to understand that Symfony2 core code is not that different from what you write in your everyday life, it still abides by its own rules.
For example, when normally creating a Twig extension in Symfony2 you need to configure it as a service, right? Well, Symfony2 core extensions are configured in the same way. You just need to find where the configuration files are located.
Following the logic from Extending the functionality we know for sure that they're not located in Component. Bridge is actually a name for a design pattern - not a place where you'd place your service configuration :)
So we're left with Bundle - and obviously that's where we find all the information we need: vendor/symfony/src/Bundle/TwigBundle/Resources/config/twig.xml
Now we simply look up how original HttpKernelExtension is configured and follow its lead:
<service id="twig.extension.httpkernel" class="%twig.extension.httpkernel.class%" public="false">
<argument type="service" id="fragment.handler" />
</service>
Transforming it into a more commonly used .yml format, our extension config could look like this:
new_func:
class: Acme\Bundle\MyBundle\Twig\NewTwigFunction
arguments:
- "#fragment.handler"
# Uncomment when implementing code from 2nd example
# - "#router"
tags:
- { name: twig.extension }
public: false
I'm migrating quite a large community to symfony2. The current user table contains a lot of users with non-alphanumeric chars in the username. In the new version I only allow [a-zA-Z0-9-] for benefits like semantic URLs for each user.
Is it possible to catch users who log in with email/pass and have no username set? I would like them to redirect to a page where they will be able to re-pick a username. The tricky part: they should not be able to touch anything on the site unless they have a correct username.
I thought about a event, from the fosuserbundle but I couldn't find a suitable one.
You could use events. See an example here: http://symfony.com/doc/2.0/cookbook/event_dispatcher/before_after_filters.html
Of course the action changing the username should be ignored by the event listener. Just like login and other anonymous actions.
You can return any response, including a redirect, by setting response on an event.
Just an idea. How about the AOP paradigm (JMSAopBundle)? Define a pointcut for you controllers (except for the login one):
class PrivateEntityInformationPointcut implements PointcutInterface
{
public function matchesClass(\ReflectionClass $class)
{
return $class->isSubclassOf('Your\Controller\Superclass')
&& $class->name !== 'Your\Controller\Access';
}
public function matchesMethod(\ReflectionMethod $method)
{
return true; // Any method
}
}
Then the interceptor should redirect to the page for setting the username:
class DenyEntityAccessInterceptor implements MethodInterceptorInterface
{
private $securityContext;
private $logger;
/**
* #DI\InjectParams({
* "securityContext" = #DI\Inject("security.context"),
* "logger" = #DI\Inject("logger"),
* })
*/
public function __construct(SecurityContext $securityContext,
Logger $logger)
{
$this->securityContext = $securityContext;
$this->logger = $logger;
}
public function intercept(MethodInvocation $invocation)
{
// Check username, redirect using the router, log what's happening
// It's OK
return $invocation->proceed();
}
}