Symfony's SessionInterface is still a little bit vague / magic for me. So if someone can enlighten me, please.
Naturally, the request contains a session object and can be used within a controller.
class SimpleController extends AbstractController
{
public function index(Request $request): Response
{
$request->getSession()->set('session-var', 10);
}
}
However, within a service you can also include the SessionInterface as a service and work with, presumably, the same ParameterBag.
class SimpleService
{
public function __construct(SessionInterface $session)
{
$session->set('session-var', 10);
}
}
My colleague pointed out that a session is (and should always be) part of the request. This makes sense to me, but also got me thinking: why are you able to us the SessionInterface as a service when it is a object/property of the request.
What I want to achieve in the end is to include a service in my controller and in this service work with the current session.
Code example of my use case would look something like this.
class SimpleController extends AbstractController
{
private $simpleService;
public function __construct(SimpleService $simpleService)
{
$this->simpleService = $simpleService;
}
public function index(Request $request): Response
{
$this->simpleService->doSomething();
}
}
class SimpleService
{
private $session;
public function __construct(SessionInterface $session)
{
$this->session = $session;
}
public function doSomething()
{
// Do some things..... and save the result in the session.
$this->session->set('just_a_preference', 50);
}
}
So:
Is this the/a correct way to work with sessions in Symfony?
Is 'just_a_preference' saved in the same session parameter bag as my $request->getSession()?
If not, what is the correct way to work with the current request session outside the controller?
If work with Symfony 4.4.
Both methods are fine. They both reference the same object. Accessing the session via the interface is preferred though. I can come up with at least three reasons why:
extend the functionality: the service behind the interface can be easily decorated – eg. LoggingSessionDecorator which would log session activity
proper OOP code: with $request->getSession()->set(…) you violate the Law of Demeter principle, that said:
it's easier to mock the session in your unit tests
In the end – yes, it doesn't matter. Both methods reference the same object
As said in 1, better to use the interface. At least in my opinion.
Related
In my Symfony 4.4 project I want to add specific method every time I trigger setSomething() setter method, but perhaps I can use traits?
Can traits have services injected?
Yes you can, but it's not injected automatically like autowiring in services.
Here's an example i made with TranslatorInterface injected in my service and use in my trait :
trait MyTrait
{
private TranslatorInterface $translator;
public function setTranslator(TranslatorInterface $translator): self
{
$this->translator = $translator;
return $this;
}
public functio myTraitFunction()
{
// use $this->translator
}
}
class MyClass
{
use MyTrait;
public function __construct(TranslatorInterface $translator)
{
$this->setTranslator($translator);
}
public function somFunction(): something
{
// use $this->myTraitFunction()
}
}
``
On a side note, maybe useful for future readers. If you have a trait based on a service, or on class member, I would go for an abstract method like
trait MyTrait
{
protected abstract function getTranslator(): TranslatorInterface;
[...]
}
and then implement the method inside the class that uses the trait.
Personally I would prefer not to depend on "setter injection" and bound two methods that are only logically coupled.
If you embrace this, the question is not actually a question as you can inject service in the canonical way.
class EtudiantController extends AbstractController
{
private $etudiant ;
private $form ;
public function __construct()
{
$this->etudiant = new Etudiant();
$this->form = $this->createForm(EtudiantType::class, new Etudiant());
}
}
** i'v got an error when instantiate a form in a constructor using the createForm() function **
Here is the wrong way to solve your problem:
class EtudiantController extends AbstractController
{
private $form;
public function __construct(FormFactoryInterface $formFactory)
{
$this->form = $formFactory->create(TextType::class, new Etudiant());
}
}
I say it is wrong (even though it will work) because creating things like forms really should be done in individual controller actions, not hidden in the constructor. You might be trying to apply Dont Repeat Yourself (DRY) but in cases like this, Don't Confuse Your Future Self takes precedence.
And as far as why injecting the form factory is necessary, I would once again urge you to look at the Symfony source code for AbstractController as well as ControllerTrait. Understanding how dependency injection works is critical to being able to effectively use the framework.
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
I got two Controllers:
class FirstController extends Controller
{
/**
* #Route("/first")
*/
public function indexAction()
{
$second = new SecondController();
$second->doit();
}
}
class SecondController extends Controller
{
public function doit()
{
$render = $this->renderView('::base.html.twig');
//write $render to disk
}
}
Instead of 'done' being written to my screen, I got error:
What am I doing wrong?
You are not supposed to instantiate controllers yourself. The framework is supposed to do that for you. If you are in controller1, and want controller2 to actually handle the request, which could use some explanation on your part, you need to forward to it, like this : $this->forward('MyBundle:MyController:myaction)
Also, you should maybe have your doit method in a service. Controllers are supposed to stay skinny, and care only about HTTP:
they receive the request
they call a service based on the request, this service should know nothing about HTTP, and be callable from a command as well as from a controller
they return a response based on what the service answers.
what is the best practice for injecting repositories, entity managers to a service?
I figured I can do it in at least three ways.
1. Injecting repositories, managers through the constructor
This way the service can be covered with tests quite easily. All you need is to pass mocked dependencies to the constructor and you're ready.
class TestService
{
public function __construct(MyEntityRepository $my, AnotherEntityRepository $another, EntityManager $manager)
{
$this->my = $my;
$this->another = $another;
$this->manager = $manager;
}
public function doSomething()
{
$item = $this->my->find(<...>);
<..>
$this->manager->persist($item);
$this->manager->flush();
}
}
2. Passing just the EntityManager
This is a bit more difficult to test if you need like 4 repositories from the same manager. I figured this way you have to mock manager's getRepository calls.
class TestService
{
public function __construct(EntityManager $manager)
{
$this->manager = $manager;
}
public function doSomething()
{
$item = $this->manager->getRepository('my')->find(<...>);
<..>
$this->manager->persist($item);
$this->manager->flush();
}
}
3. Passing the whole registry
This way you don't get circular reference exception with doctrine event subscribers, but it's harder to mock everything.
Also this is the only way sensiolabs insights doesn't give me an architectural violation for injecting EntityManager.
class TestService
{
public function __construct(RegistryInterface $registry)
{
$this->doctrine = $registry;
}
public function doSomething()
{
$item = $this->registry->getManager()->getRepository('my')->find(<...>);
<..>
$this->registry->getManager()->persist($item);
$this->registry->getManager()->flush();
}
}
What is the best practice to do this and why?
I always try to inject my services as specific as possible.
Which means I always inject repositories since that is easier when writing tests. Otherwise you have to mock the registry and or manager too.
I know this is old but I just thought I'd add my 2 cents. I follow what Matthias Noback says in these two blog posts:
Inject a repository instead of the EntityManager
Inject the ManagarRegistry instead of the EntityManager
So I inject the specific repository whenever I need to find an entity, but I also inject the ManagerRegistry if I need to call flush, persist or remove, because otherwise you have to put proxy functions for them in all your repositories.