Symfony2 workaround when devices have cookies disabled - php

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.

Related

How should one implement a custom Doctrine purger when using Symfony 6?

The Symfony docs shows a solution, but it doesn't appear to work (i.e. Doctrine\Bundle\FixturesBundle\Purger\PurgerFactory needs to be replaced with Doctrine\Bundle\FixturesBundle\Purger\ORMPurgerFactory, and other changes). I modified the code as shown below, but am pretty certain I am not doing it correctly.
<?php
declare(strict_types=1);
namespace App\DataFixtures\Purger;
use Doctrine\Bundle\FixturesBundle\Purger\PurgerFactory;
use Doctrine\Common\DataFixtures\Purger\PurgerInterface;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Bundle\FixturesBundle\Purger\ORMPurgerFactory;
class CustomPurgerFactory implements PurgerFactory
{
public function __construct(private ORMPurgerFactory $purgeFactory)
{
}
public function createForEntityManager(?string $emName, EntityManagerInterface $em, array $excluded = [], bool $purgeWithTruncate = false) : PurgerInterface
{
// Change $excluded, $purgeWithTruncate as desired.
return new CustomPurger($emName, $em, $excluded, $purgeWithTruncate, $this->purgeFactory);
}
}
<?php
declare(strict_types=1);
namespace App\DataFixtures\Purger;
use Doctrine\Common\DataFixtures\Purger\PurgerInterface;
use Doctrine\Common\DataFixtures\Purger\ORMPurgerInterface;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Bundle\FixturesBundle\Purger\ORMPurgerFactory;
class CustomPurger implements ORMPurgerInterface
{
public function __construct(private ?string $emName, private EntityManagerInterface $entityManager, private array $excluded, private bool $purgeWithTruncate, private ORMPurgerFactory $purgeFactory)
{
}
public function setEntityManager(EntityManagerInterface $entityManager):void
{
// Seems rather redundent doing this even though I earlier inject $entityManager.
$this->entityManager = $entityManager;
}
public function purge() : void
{
// Delete any tables which must be deleted first to prevent FK constraint errors.
// This doesn't seem write.
$purger = $this->purgeFactory->createForEntityManager($this->emName, $this->entityManager, $this->excluded, $this->purgeWithTruncate);
$purger->purge();
}
}
services:
App\DataFixtures\Purger\DoctrinePurgerFactory:
tags:
- { name: 'doctrine.fixtures.purger_factory', alias: 'my_purger' }
arguments:
- '#doctrine.fixtures.purger.orm_purger_factory'
Or should it be done by decorating the default purger as suggested by this post?
Okay. So you do have a few things wrong and the docs are somewhat out of date. From a big picture point of view you want something like:
bin/console doctrine:fixtures:load --purger=my_purger
to use your custom purger factory (aliased as my_purger) to instantiate and execute your custom purger's purge method. The job of the factory is to just create the purger not to execute it.
I followed the docs and implemented PurgerInterface but the purge command complained about it not implementing ORMPurgerInterface which, as you noted, adds a seemingly superfluous method. I think it is still a work in progress. The default ORMPurger has a couple of additional public methods not defined in any interface which is also strange. The fact that Doctrine is inconsistent with it's usage of the Interface suffix does not help. But it is what it is.
This works under 6.1:
# CustomPurger.php
use Doctrine\Common\DataFixtures\Purger\ORMPurgerInterface;
use Doctrine\ORM\EntityManagerInterface;
class CustomPurger implements ORMPurgerInterface
{
private EntityManagerInterface $em;
public function setEntityManager(EntityManagerInterface $em) : void
{
$this->em = $em;
}
public function purge() : void
{
dd(' my purger');
}
}
# CustomPurgerFactory.php
use Doctrine\Bundle\FixturesBundle\Purger\PurgerFactory;
use Doctrine\Common\DataFixtures\Purger\PurgerInterface;
use Doctrine\ORM\EntityManagerInterface;
class CustomPurgerFactory implements PurgerFactory
{
public function createForEntityManager(?string $emName, EntityManagerInterface $em, array $excluded = [], bool $purgeWithTruncate = false) : PurgerInterface
{
return new CustomPurger($em);
}
}
# services.yaml
App\Purger\CustomPurgerFactory:
tags:
- { name: 'doctrine.fixtures.purger_factory', alias: 'my_purger' }
bin/console doctrine:fixtures:load --purger=my_purger
> purging database
^ " my purger"
As far as decorating goes, you decorate a service when you want to modify some methods without extending the original class. There is only one method here and it's quite a doozy so I don't think decorating will help.
If you wanted to always use your purger without the --purger option then you could probably point the default purger factory service id to your factory. I'll leave that as an exercise for the reader.
One final note: I took a look at your decorating link. Don't know what they were trying to do but I do know it has nothing to do with decorating.

How can I get a collection of all monolog loggers in Symfony?

I have a factory class that creates different objects and I have a lot of loggers defined for each type of object.
And I want to get all loggers collection in my factory. Before I just used a ContainerInterface in my factory constructor, but since Symfony 5.1 container autowiring is deprecated.
Now I can not find a way to get a collection of loggers. I tried to use
!tagged_iterator { tag: 'monolog.logger' }
and also tried to set a tag for LoggerInterface and get a tagged_iterator for it, but it didn't work. I suggest that it is because loggers are not real classes.
This might be overkill but as you say the logger services are a bit unusual. Seems like they should be tagged or be taggable by LoggerInterface but I ran some test and could not get it to work. Here is a brute force approach which relies on logger services having ids of monolog.logger.name:
# Start with a service locator class
namespace App\Service;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ServiceLocator;
class LoggerLocator extends ServiceLocator
{
// Just to specify the return type to keep IDEs happy
public function get($id) : LoggerInterface
{
return parent::get($id);
}
}
...
# now make the kernel into a compiler pass
# src/Kernel.php
class Kernel extends BaseKernel implements CompilerPassInterface
...
public function process(ContainerBuilder $container)
{
$loggerServices = [];
foreach($container->getServiceIds() as $id) {
if (!strncmp($id,'monolog.logger.',15)) {
//echo 'Logger ' . $id . "\n";
$loggerServices[$id] = new Reference($id);
}
}
$loggerLocator = $container->getDefinition(LoggerLocator::class);
$loggerLocator->setArguments([$loggerServices]);
}
# and now we can inject the locator where it is needed
class SomeController {
public function index(Request $request, LoggerLocator $loggerLocator)
{
dump($loggerLocator);
$logger = $loggerLocator->get('monolog.logger.' . $name);
Seems like there should be a way to do this just through configuring but a pass is easy enough I guess.

PHPUnit: How to test Doctrine Repository method that returns void?

I have a Doctrine repository class that can be used to persist a User, I don't want to check if the entity was really persisted or not, I just want to know what to do in this situation:
It's easy to test a repository method that has a return value. But in this case, I have nothing to do, and I'd like to cover 100% of the code, without making unsafe code that can break like use #addToAssertionCount.
<?php
namespace Domain\Repository;
use DateTime;
use Domain\Entity\User;
use Domain\Repository\Interfaces\UserRepositoryInterface;
class UserRepository extends Repository implements UserRepositoryInterface
{
public function create(User $user): void
{
$user->setCreatedAt(new DateTime('now'));
$this->getEntityManager()->persist($user);
}
}
And a testing class for it:
<?php
namespace Domain\Repository;
use Doctrine\ORM\EntityManager;
use Domain\Entity\User;
use PHPUnit\Framework\TestCase;
class UserRepositoryTest extends TestCase
{
private UserRepository $sut;
public function setUp(): void
{
$entity_manager = $this->createMock(EntityManager::class);
$this->sut = new UserRepository($entity_manager);
}
public function test_assert_create(): void
{
$user = $this->createMock(User::class);
$this->sut->create($user);
// What to assert??
}
}
At this point, I don't know even what to assert, once the persist() method returns void, which I can't mock.
Focusing on 100% code coverage is not a good idea and encourages writing tests that have little to no value. What does that mean? The create method has two side effects: It changes the users creation date and persists it. You could test it like this:
final class UserRepositoryTest extends TestCase
{
/**
* #var EntityManager&MockObject
*/
private EntityManager $entityManager;
private UserRepository $sut;
public function setUp(): void
{
$this->entityManager = $this->createMock(EntityManager::class);
$this->sut = new UserRepository($this->entityManager);
}
public function test_create_should_persist_entity(): void
{
$user = new User();
$user->setCreatedAt(new DateTime('2000-01-01 12:15:30'));
// validate that persist call was made
$this->entityManager->expects(self::once())
->method('persist')
->with($user);
$this->sut->create($user);
// validate that creation date was set
self::assertEqualsWithDelta(new DateTime('now'), $user->getCreatedAt(), 3);
}
}
You could go even one step further and verify that the creation date was set before the persist call was made by using the callback constraint. But then, you're pretty much checking the implementation line by line in your test. That's how people end with tests that break all the time.
So, what to do instead? Focus on the purpose of the user repository: If you put something in, you should be able to get it out. This, however, requires that you use an actual entity manager. But you wrote, that you don't want to check that the entity was actually persisted. In that case, I would rather write no tests than the example I gave above.

Laravel: How to set globally available default route parameters

I'm trying to set a handful of default route parameters that will work globally in my application regardless of context. In the documentation for URL generation the example given is using middleware which is fine for HTTP, but won't get called during non-HTTP contexts. I also need this to work when called from the CLI.
My first idea is to have a Service Provider that calls the defaults method on boot:
<?php
namespace App\Providers;
use Illuminate\Routing\UrlGenerator;
use Illuminate\Support\ServiceProvider;
class UrlDefaults extends ServiceProvider
{
public function boot(UrlGenerator $urlGenerator): void
{
$urlGenerator->defaults([
'foo' => 'abc',
'bar' => 'xyz',
]);
}
}
But this does not work for HTTP requests:
Route::get('test', function (\Illuminate\Routing\UrlGenerator $urlGenerator) {
dump($urlGenerator->getDefaultParameters());
});
Outputs []
I believe this is because in the UrlGenerator, the setRequest method unconditionally sets the routeGenerator property to null. My Service Provider's boot method is called during the bootstrapping process, but then the request is set afterwards clobbering my defaults.
//Illuminate/Routing/UrlGenerator.php
public function setRequest(Request $request)
{
$this->request = $request;
$this->cachedRoot = null;
$this->cachedSchema = null;
$this->routeGenerator = null;
}
Dumping the UrlGenerator during boot and then again in my routes file can demonstrate this:
As you can see, the UrlGenerator instance is the same both times, but the RouteUrlGenerator on the routeGenerator property has changed.
I am unsure of a better way to set these defaults.
Not sure why this is getting attention almost a year later, but I ended up finding a solution by myself.
To add a bit more information to the original question, the purpose of this was to allow us to have the same instance of the code powering both our live and sandbox application. There's more involved to get this working, but this issue was just about URL generation for links in views. All links generated always both a subdomain and tld, so this code injects these values always.
These views are rendered both as a response to a HTTP request, e.g. in our client areas, but also as part of a non HTTP request, e.g. a scheduled task generating invoices and emailing them to clients.
Anyway, the solution:
For non HTTP contexts, a service provider can set the defaults:
<?php namespace App\Providers;
use App\Support\UrlDefaults;
use Illuminate\Routing\UrlGenerator;
use Illuminate\Support\ServiceProvider;
class UrlDefaultsServiceProvider extends ServiceProvider
{
public function boot(UrlGenerator $urlGenerator): void
{
$urlGenerator->defaults(UrlDefaults::getDefaults());
}
}
Since the there's no routing going on to cause the problem I asked originally, this just works.
For HTTP contexts, the RouteMatched event is listened for and the defaults injected then:
<?php namespace App\Listeners;
use App\Support\UrlDefaults;
use Illuminate\Routing\Router;
use Illuminate\Routing\UrlGenerator;
/**
* Class SetUrlDefaults
*
* This class listeners for the RouteMatched event, and when it fires, injects the route paramaters (subdomain, tld,
* etc) into the defaults of the UrlGenerator
*
* #package App\Listeners
*/
class SetUrlDefaults
{
private $urlGenerator;
private $router;
public function __construct(UrlGenerator $urlGenerator, Router $router)
{
$this->urlGenerator = $urlGenerator;
$this->router = $router;
}
public function handle(): void
{
$paramaters = array_merge(UrlDefaults::getDefaults(), $this->router->current()->parameters);
$this->urlGenerator->defaults($paramaters);
}
}
UrlDefaults is just a simple class that returns an array:
<?php namespace App\Support;
class UrlDefaults
{
public static function getDefaults(): array
{
return [
'tld' => config('app.url.tld'),
'api' => config('app.url.api'),
'foo' => config('app.url.foo'),
'bar' => config('app.url.bar'),
];
}
}
So digging into the source for routing classes a bit more, there’s a defaults() method on the UrlGenerator class, but it’s not a singleton, so any defaults you set in a service provider aren’t persisted.
I seem to have got it working by setting the defaults in some middleware:
Route::domain('{domain}')->middleware('route.domain')->group(function () {
//
});
namespace App\Http\Middleware;
use Illuminate\Contracts\Routing\UrlGenerator;
class SetRouteDomain
{
private $url;
public function __construct(UrlGenerator $url)
{
$this->url = $url;
}
public function handle($request, Closure $next)
{
$this->url->defaults([
'domain' => $request->getHost(),
]);
return $next($request);
}
}

Get request service from a DataTransformer class

Short story:
I need to get the Request service from a class that doesn't inherit from the Controller class (it's a DataTransformer which -obviously- implements the DataTransformerInterface).
Long story:
I have an embedded form that has an email field. If the user enters an email which doesn't exists in my users database table, I want to create a new user with this email.
In order to do that, I need to set its IP, so I followed the embedded forms tutorial and the data transformer recipe, but finally I have no idea where I'm able to inject the Request instance to my DataTransformer constructor or something else.
If I was in a class extending form the Controller one, it would be as simple as:
$this->container->get('request')->getClientIp()
You can do this by "Referencing (Injecting) Services". In your case you want to inject the Request which is a service from a narrower scope.
If you are using transformers, you are probably already using a Custom Form Type, and are instantiating the Data Transformer within your Form Type BuildForm Method, see here for more info.
You want to inject the Request object to the custom Form Type, then it can passed to the Data Transformer as a constructor parameter.
To do this modify the services.yml file with in your bundle, and add a constructor to the Custom Form Type and the Custom Data Transformer like this:
// src/Acme/HelloBundle/Resources/config/services.yml
parameters:
// ...
services:
acme.type.custom_type:
class: Acme\HelloBundle\Form\Type\CustomType
scope: request
arguments: ["#doctrine.odm.mongodb.document_manager", "#request"]
tags:
- { name: form.type, alias: custom_type }
The update the CustomType Class like this:
<?php
// src/Acme/HelloBundle/Form/Type/CustomType.php
namespace Acme\HelloBundle\Form\Type;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Doctrine\ODM\MongoDB\DocumentManager;
use Acme\HelloBundle\Form\DataTransformer\CustomDataTransformer;
class CustomType extends AbstractType
{
private $request;
private $dm;
public function __construct(DocumentManager $dm, Request $request) {
$this->dm = $dm;
$this->request = $request;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
// Create a new Data Transformer that will be able to use the Request Object!
$transformer = new CustomDataTransformer($this->dm, $this->request);
$builder->addModelTransformer($transformer);
}
// ...
}
and finally add a constructor to the transformer similar to the one added in the Form Type:
<?php
// src/Acme/HelloBundle/Form/DataTransformer/CustomDataTransformer.php
namespace Acme\HelloBundle\Form\DataTransformer;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Form\DataTransformerInterface;
use Doctrine\ODM\MongoDB\DocumentManager;
class CustomDataTransformer implements DataTransformerInterface
{
private $request;
private $dm;
public function __construct(DocumentManager $dm, Request $request) {
$this->dm = $dm;
$this->request = $request;
}
// ...
}
Notice that along with the Request I have injected the MongoDB DocumentManager, this is to show that multiple objects can be injected.
Ok, that's simple:
In my question I was assuming that the DataTransformer will be "magically" invoked, but it's instanced while building the form, so if it helps to anyone, here it is:
In the DataTransformer class (implementing the DataTransformerInterface):
Define the new class attributes in order to hold the dependency injection:
/**
* #var EntityManager
*/
private $entityManager;
/**
* #var \Symfony\Component\DependencyInjection\Container
*/
private $container;
Define the constructor like:
public function __construct( EntityManager $entityManager, Container $container )
{
$this->entityManager = $entityManager;
$this->container = $container;
}
In your form class (implementing the AbstractType)
Add the following calls to the setDefaultOptions method:
$resolver->setRequired( array( 'em', 'container' ) );
$resolver->setAllowedTypes( array(
'em' => 'Doctrine\Common\Persistence\ObjectManager',
'container' => 'appDevDebugProjectContainer',
) );
In the buildForm method, apply the transformer as defined in the transformer recipe but instance it as:
$entityManager = $options['em'];
$container = $options['container'];
$transformer = new FantasticTransformer( $entityManager, $container );
In your controller, when you're calling to the createForm method, is it possible to inject the EntityManager and the Container instances simply adding them as follows:
$form = $this->createForm( 'your_form', $lookup, array(
'action' => $this->generateUrl( 'your_action_url' ),
'em' => $this->getDoctrine()->getManager(),
'container' => $this->container
) );
Now, you can finally get the client IP from the request service calling to the container defined in the constructor of your DataTransformer class:
$ip = $this->container->get('request')->getClientIp();
Note that we're injecting the container instead of the request instance, it's due to the Symfony scopes.

Categories