How would I go about binding a Symfony config tree to a class rather than returning an array?
Using Symfony\Component\Config\Definition\Processor returns an array.
In my case I want the config to be bound to a class so I can use methods to combine parts of the data.
Here is a simple example of my use case. I want the config bound to a class so I can use a method to join table.name and table.version together (my actual use case is more complex, but this is a simple example)
config.yml
db:
table:
name: some_table
version: v2
ConfigurationInterface
class DBConfiguration implements ConfigurationInterface
{
/**
* {#inheritDoc}
*/
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('db');
$rootNode
->children()
->arrayNode('table')
->children()
->scalarNode('name')->isRequired()->end()
->scalarNode('version')->end()
->end()
->end()
;
return $treeBuilder;
}
}
Class I want to bind the config to
class DB
{
public $table;
public function __construct()
{
$this->table = new Table();
}
}
class Table
{
public $name;
public $version;
/**
* #return string
* Calculate the full table name.
*/
public function getTableName()
{
return $this->name.'-'.$this->version;
}
}
The Symfony Config component doesn't support that.
However, in a Symfony project, this is usually done at the container compile phase. In your bundle's Extension class, you will have access to the configuration tree of your bundle in array form.
You can then take this array and assign it to a service defined in the service container that will create your config object.
This is exactly how DoctrineBundle's configuration class is built:
Abstract services (for the configuration and the factory) are defined in dbal.xml
When loading DoctrineBundle's extension, an instance of the abstract config service is created for each defined connection.
An instance of the abstract factory service is created for each defined connection.
The options array is then passed to the abstract factory service along with the configuration
When creating an instance, the factory then does the necessary transformations.
As far as I know, Symfony has no native support for this, however, you could implement it yourself. You could use subset of Symfony Serializer Component in charge of deserialization, but I think it would be an overkill. Especially since I don't see any PublicPropertyDenormalizer, only GetSetMethodNormalizer (which is denormalizer too). Therefor you would have to either make your config objects have get/set methods or roll PublicPropertyDenormalizer on your own. Possible but it really seems like an overkill and doesn't look like helping much:
Symfony Serializer Component
$array = [
'field1' => 'F1',
'subobject' => [
'subfield1' => 'SF1',
],
];
class MyConfigObject implements Symfony\Component\Serializer\Normalizer\DenormalizableInterface
{
private $field1;
private $subobject;
public function getField1()
{
return $this->field1;
}
public function setField1($field1)
{
$this->field1 = $field1;
}
public function getSubobject()
{
return $this->subobject;
}
public function setSubobject(SubObject $subobject)
{
$this->subobject = $subobject;
}
public function denormalize(\Symfony\Component\Serializer\Normalizer\DenormalizerInterface $denormalizer, $data, $format = null, array $context = array())
{
$obj = new static();
$obj->setField1($data['field1']);
$obj->setSubobject($denormalizer->denormalize($data['subobject'], 'SubObject'));
return $obj;
}
}
class SubObject
{
private $subfield1;
public function getSubfield1()
{
return $this->subfield1;
}
public function setSubfield1($subfield1)
{
$this->subfield1 = $subfield1;
}
}
$normalizer = new \Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer();
$obj = (new MyConfigObject())->denormalize($normalizer, $array);
Native PHP Way
Imo this is a lot easier than above as Symfony Serializer wasn't really ment for that.
$array = [
'field1' => 'F1',
'subobject' => [
'subfield1' => 'SF1',
],
];
trait Denormalizable
{
public function fromArray($array)
{
foreach ($array as $property => $value) {
if (is_array($value)) {
if ($this->$property instanceof ArrayDenormalizableInterface) {
$this->$property->fromArray($value);
} else {
$this->$property = $value;
}
} else {
$this->$property = $value;
}
}
}
}
interface ArrayDenormalizableInterface
{
public function fromArray($array);
}
class MyConfigObject implements ArrayDenormalizableInterface
{
use Denormalizable;
public $field1;
public $subobject;
public function __construct()
{
$this->subobject = new SubObject();
}
}
class SubObject implements ArrayDenormalizableInterface
{
use Denormalizable;
public $subfield1;
}
$myConf = new MyConfigObject();
$myConf->fromArray($array);
Whatever way you choose, you can now just take array returned from symfony processor and turn it into a config object you need.
Related
Since Symfony 6.1, instead of creating an extension class and another configuration class, our bundle class can now extend AbstractBundle to add this logic to the bundle class directly. This abstract class provides configuration hooks.
// src/AcmeSocialBundle.php
namespace Acme\SocialBundle;
use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;
class AcmeSocialBundle extends AbstractBundle
{
public function configure(DefinitionConfigurator $definition): void
{
$definition->rootNode()
->children()
->arrayNode('twitter')
->children()
->integerNode('client_id')->end()
->scalarNode('client_secret')->end()
->end()
->end() // twitter
->end()
;
}
public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void
{
// Contrary to the Extension class, the "$config" variable is already merged
// and processed. You can use it directly to configure the service container.
$container->services()
->get('acme.social.twitter_client')
->arg(0, $config['twitter']['client_id'])
->arg(1, $config['twitter']['client_secret'])
;
}
}
Symfony documentation explains how to test no more used configuration class.
class ConfigurationTest extends TestCase
{
public function testEmptyConfiguration(): void
{
$config = Yaml::parse('');
$processor = new Processor();
$result = $processor->processConfiguration(
new Configuration(),
[$config]
);
self::assertSame($result, ['foo' => 'bar'],
]);
}
But how to test my $definition in the configure method of this new pattern?
In this case, we need to find a way to access the bundle configuration so that we can process and test it. There is usually a fixed configuration class behind the new structure (Symfony\Component\Config\Definition\Configuration), so the only thing you would need to do is retrieve it:
class ConfigurationTest extends TestCase
{
public function testEmptyConfig(): void
{
$configuration = (new AcmeSocialBundle())
->getContainerExtension()
->getConfiguration([], new ContainerBuilder(new ParameterBag()))
;
$configs = [Yaml::parse('...')];
$result = (new Processor())->processConfiguration($configuration, $configs);
self::assertSame(['foo' => 'bar'], $result);
}
}
Note that the $configs variable should be an array of arrays (array of configs).
Actually, I have a listener on AuthenticationEvents::AUTHENTICATION_FAILURE that stores failedLogin in Redis Cache, like:
[
'ip' => [
'xxx.xxx.xxx.xxx' => [
'nbAttempts' => 5,
'lastAttempd' => \DateTime
],
],
'username' => [
'my_login' => [
'nbAttempts' => 3,
'lastAttempd' => \DateTime
],
'my_other_login' => [
'nbAttempts' => 2,
'lastAttempd' => \DateTime
],
]
]
But now, I need to use this list of fails to prevent logins when a user try to connect with a username tries more than x times in n minutes, and the same for an IP (with an other ratio). (later, maybe add a ReCaptcha before block)
To do it, I need to add a custom validation rules on the login. I've found it in the documentation:
http://symfony.com/doc/current/security/custom_password_authenticator.html
https://symfony.com/doc/current/security/guard_authentication.html
But, in both documents, I need to rewrite a lot of things, but I want to keep all the actual behaviors: redirect user on previous page (with referer or on a default page), remember me (in the gurad, I'me forced to return a response on success, else remember me don't work, but I don't really know which response return.... Because if I return null, the redirection work well), messages, etc...
I've search but not found the guard used per default by Symfony to copy/paste it, and just add one rule.
Someone know an other manner, that just consist to rewrite the checkCredential ?
Thanks a lot
EDIT (see the answer at the end):
I've found an advanced guard abstract class: Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator.Then, the authentication work like in Symfony, now, I just need to add my own test in checkCredentials (in my case in the getUser(), I prefer return the error before retrieve the user.
You can listen on the event for failed login attempts. Create a service:
services:
app.failed_login_listener:
class: AppBundle\EventListener\AuthenticationFailureListener
tags:
- { name: kernel.event_listener, event: security.authentication.failure, method: onAuthenticationFailure }
Then create the listener:
<?php
namespace App\EventListener;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
class AuthenticationFailureListener implements AuthenticationFailureHandlerInterface
{
public function onAuthenticationFailure(
Request $request,
AuthenticationException $exception
) {
// do whatever
}
}
Modify your service definition to inject whatever other services you may need.
If you want to perform actions after the user logs in, you can do that with the security.interactive_login event. Just throw exceptions if you encounter situations where you want the void the user's login, and perhaps remove their security token or whatever else you need. You could even do this in your Controller's login action.
For example:
services:
app.security_listener:
class: AppBundle\EventListener\InteractiveLoginListener
tags:
- { name: kernel.event_listener, event: security.interactive_login, method: onInteractiveLogin }
Then have your listener:
<?php
namespace App\EventListener;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
class InteractiveLoginListener
{
public function onInteractiveLogin(InteractiveLoginEvent $event)
{
// do whatever
}
}
Again inject dependencies as needed. Also look at Symfony's creating a custom authentication provider documentation.
Finally, I've found a simply way to do by extending this abstract class: Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator. This Authenticator replace the default FormLoginAuthenticator used by Symfony, but is very simple, and we just rewrite few methods.
Maybe just found a way to get config.yml value, to define routes (avoid to write it in this file, because we declare it in config).
My service declaration:
app.security.form_login_authenticator:
class: AppBundle\Security\FormLoginAuthenticator
arguments: ["#router", "#security.password_encoder", "#app.login_brute_force"]
My FormLoginAuthenticator:
<?php
namespace AppBundle\Security;
use AppBundle\Utils\LoginBruteForce;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Router;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
class FormLoginAuthenticator extends AbstractFormLoginAuthenticator
{
private $router;
private $encoder;
private $loginBruteForce;
public function __construct(Router $router, UserPasswordEncoderInterface $encoder, LoginBruteForce $loginBruteForce)
{
$this->router = $router;
$this->encoder = $encoder;
$this->loginBruteForce = $loginBruteForce;
}
protected function getLoginUrl()
{
return $this->router->generate('login');
}
protected function getDefaultSuccessRedirectUrl()
{
return $this->router->generate('homepage');
}
public function getCredentials(Request $request)
{
if ($request->request->has('_username')) {
return [
'username' => $request->request->get('_username'),
'password' => $request->request->get('_password'),
];
}
return;
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$username = $credentials['username'];
// Check if the asked username is under bruteforce attack, or if client process to a bruteforce attack
$this->loginBruteForce->isBruteForce($username);
// Catch the UserNotFound execption, to avoid gie informations about users in database
try {
$user = $userProvider->loadUserByUsername($username);
} catch (UsernameNotFoundException $e) {
throw new AuthenticationException('Bad credentials.');
}
return $user;
}
public function checkCredentials($credentials, UserInterface $user)
{
// check credentials - e.g. make sure the password is valid
$passwordValid = $this->encoder->isPasswordValid($user, $credentials['password']);
if (!$passwordValid) {
throw new AuthenticationException('Bad credentials.');
}
return true;
}
}
And, if it's interesting someone, my LoginBruteForce:
<?php
namespace AppBundle\Utils;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
class LoginBruteForce
{
// Define constants used to define how many tries we allow per IP and login
// Here: 20/10 mins (IP); 5/10 mins (username)
const MAX_IP_ATTEMPTS = 20;
const MAX_USERNAME_ATTEMPTS = 5;
const TIME_RANGE = 10; // In minutes
private $cacheAdapter;
private $requestStack;
public function __construct(AdapterInterface $cacheAdapter, RequestStack $requestStack)
{
$this->cacheAdapter = $cacheAdapter;
$this->requestStack = $requestStack;
}
private function getFailedLogins()
{
$failedLoginsItem = $this->cacheAdapter->getItem('failedLogins');
$failedLogins = $failedLoginsItem->get();
// If the failedLogins is not an array, contruct it
if (!is_array($failedLogins)) {
$failedLogins = [
'ip' => [],
'username' => [],
];
}
return $failedLogins;
}
private function saveFailedLogins($failedLogins)
{
$failedLoginsItem = $this->cacheAdapter->getItem('failedLogins');
$failedLoginsItem->set($failedLogins);
$this->cacheAdapter->save($failedLoginsItem);
}
private function cleanFailedLogins($failedLogins, $save = true)
{
$actualTime = new \DateTime('now');
foreach ($failedLogins as &$failedLoginsCategory) {
foreach ($failedLoginsCategory as $key => $failedLogin) {
$lastAttempt = clone $failedLogin['lastAttempt'];
$lastAttempt = $lastAttempt->modify('+'.self::TIME_RANGE.' minute');
// If the datetime difference is greatest than 15 mins, delete entry
if ($lastAttempt <= $actualTime) {
unset($failedLoginsCategory[$key]);
}
}
}
if ($save) {
$this->saveFailedLogins($failedLogins);
}
return $failedLogins;
}
public function addFailedLogin(AuthenticationFailureEvent $event)
{
$clientIp = $this->requestStack->getMasterRequest()->getClientIp();
$username = $event->getAuthenticationToken()->getCredentials()['username'];
$failedLogins = $this->getFailedLogins();
// Add clientIP
if (array_key_exists($clientIp, $failedLogins['ip'])) {
$failedLogins['ip'][$clientIp]['nbAttempts'] += 1;
$failedLogins['ip'][$clientIp]['lastAttempt'] = new \DateTime('now');
} else {
$failedLogins['ip'][$clientIp]['nbAttempts'] = 1;
$failedLogins['ip'][$clientIp]['lastAttempt'] = new \DateTime('now');
}
// Add username
if (array_key_exists($username, $failedLogins['username'])) {
$failedLogins['username'][$username]['nbAttempts'] += 1;
$failedLogins['username'][$username]['lastAttempt'] = new \DateTime('now');
} else {
$failedLogins['username'][$username]['nbAttempts'] = 1;
$failedLogins['username'][$username]['lastAttempt'] = new \DateTime('now');
}
$this->saveFailedLogins($failedLogins);
}
// This function can be use, when the user reset his password, or when he is successfully logged
public function resetUsername($username)
{
$failedLogins = $this->getFailedLogins();
if (array_key_exists($username, $failedLogins['username'])) {
unset($failedLogins['username'][$username]);
$this->saveFailedLogins($failedLogins);
}
}
public function isBruteForce($username)
{
$failedLogins = $this->getFailedLogins();
$failedLogins = $this->cleanFailedLogins($failedLogins, true);
$clientIp = $this->requestStack->getMasterRequest()->getClientIp();
// If the IP is in the list
if (array_key_exists($clientIp, $failedLogins['ip'])) {
if ($failedLogins['ip'][$clientIp]['nbAttempts'] >= self::MAX_IP_ATTEMPTS) {
throw new AuthenticationException('Too many login attempts. Please try again in '.self::TIME_RANGE.' minutes.');
}
}
// If the username is in the list
if (array_key_exists($username, $failedLogins['username'])) {
if ($failedLogins['username'][$username]['nbAttempts'] >= self::MAX_USERNAME_ATTEMPTS) {
throw new AuthenticationException('Maximum number of login attempts exceeded for user: "'.$username.'". Please try again in '.self::TIME_RANGE.' minutes.');
}
}
return;
}
}
Symfony 2.8.13 / Doctrine ORM 2.5.5 / PHPUnit 5.7.5
I want to test a method of a class that makes use of the doctrine entity manager. This public method calls a private one that instantiates a Bookmark entity, flushes it and returns this entity. Then later, in the tested method I need to access the entity Id. Everything is mocked excepted the Bookmark entity itself. The main problem is that there is no setId() method in my entity. Here is the code and my main idea to solve this issue but I don't know if it is correct ?
Tested class and method
class BookmarkManager
{
//...
public function __construct(TokenStorageInterface $tokenStorage, ObjectManager $em, Session $session)
{
//...
}
public function manage($bookmarkAction, $bookmarkId, $bookmarkEntity, $bookmarkEntityId)
{
//...
$bookmark = $this->add($bookmarkEntity, $bookmarkEntityId);
//...
$bookmarkId = $bookmark->getId();
//...
}
private function add($entity, $entityId)
{
//...
$bookmark = new Bookmark();
//...
$this->em->persist($bookmark);
$this->em->flush();
return $bookmark;
}
}
Test
class BookmarkManagerTest extends \PHPUnit_Framework_TestCase
{
public function testThatRestaurantAdditionToBookmarksIsWellManaged()
{
//...
// THIS WON'T WORK AS NO setId() METHOD EXISTS
$entityManagerMock->expects($this->once())
->method('persist')
->will($this->returnCallback(function ($bookmark) {
if ($bookmark instanceof Bookmark) {
$bookmark->setId(1);
}
}));
//...
$bookManager = new BookmarkManager($tokenStorageMock, $entityManagerMock, $sessionMock);
//...
}
}
Solutions ?
1- Make usage of reflection class as proposed here :
$entityManagerMock->expects($this->once())
->method('persist')
->will($this->returnCallback(function ($bookmark) {
if ($bookmark instanceof Bookmark) {
$class = new \ReflectionClass($bookmark);
$property = $class->getProperty('id');
$property->setAccessible(true);
$property->setValue($bookmark, 1);
//$bookmark->setId(1);
}
}));
2- Create a test Boookmark entity that extends from the real one and add a setId() method. Then create a mock of this class and replace and customize the one got from the ReturnCallback method with this one ? It seems crappy...
Any thoughts ? Thanks for your help.
The reflection looks interesting but it decreases readability of tests (mixing with mocks makes the situation tough).
I would create a fake for entity manager and implements there setting id based on reflection:
class MyEntityManager implements ObjectManager
{
private $primaryIdForPersitingObject;
public function __construct($primaryIdForPersitingObject)
{
$this->primaryIdForPersitingObject = $primaryIdForPersitingObject;
}
...
public function persist($object)
{
$reflectionClass = new ReflectionClass(get_class($object));
$idProperty = $reflectionClass->getProperty('id');
$idProperty->setAccessible(true);
$idProperty->setValue($object, $this->primaryIdForPersitingObject);
}
public function flush() { }
...
}
Once you implemented this, you can inject the instance of MyEntityManager and make your tests small and easier to maintain.
You test would look like
<?php
class BookmarkManagerTest extends \PHPUnit_Framework_TestCase
{
public function testThatRestaurantAdditionToBookmarksIsWellManaged()
{
// ...
$entityManager = MyEntityManager(1);
//...
$bookManager = new BookmarkManager($tokenStorageMock, $entityManager, $sessionMock);
//...
}
}
Of course, a situation may be harder if there is a need of setting different ids for many persisting objects. Then you can, for example, increase $primaryIdForPersitingObject on persist call
public function persist($object)
{
$reflectionClass = new ReflectionClass(get_class($object));
$idProperty = $reflectionClass->getProperty('id');
$idProperty->setAccessible(true);
$idProperty->setValue($object, $this->primaryIdForPersitingObject);
$this->primaryIdForPersitingObject++;
}
It may be extended even further to have separate primaryIdForPersitingObject each entity class, and your tests will be still clean.
I have this situation
abstract class Importer {
const NW = 1;
public static function getInstance($type)
{
switch($type)
{
case(self::NW):
return new NWImporter();
break;
}
}
protected function saveObject(myObject $myObject)
{
//here I need to use doctrine to save on mongodb
}
abstract function import($nid);
}
and
class NWImporter extends Importer
{
public function import($nid)
{
//do some staff, create myObject and call the parent method to save it
parent::saveObject($myObject);
}
}
and I want to use them like this
$importer = Importer::getInstance(Importer::NW);
$importer->import($nid);
my question is: how to inject doctrine to be used in saveObject method?
thanks
You need to configure your importer as a symfony service :
services:
test.common.exporter:
# put the name space of your class
class: Test\CommonBundle\NWImporter
arguments: [ "#doctrine" ]
then in NWImporter define a constructor with a parameter that will have the doctrine instance
public function __construct($doctrine)
{
$this->doctrine= $doctrine;
}
with this solution you can avoid using a factory method as symfony does it for you but if you wanna to keep it, When you call $importer = Importer::getInstance(Importer::NW); from your controller you can inject the doctrine argument in your factory method :
abstract class Importer {
const NW = 1;
public static function getInstance($type, $doctrine)
{
switch($type)
{
case(self::NW):
return new NWImporter($doctrine);
break;
}
}
protected function saveObject(myObject $myObject)
{
//here I need to use doctrine to save on mongodb
}
abstract function import($nid);
}
then in your controller you should to do something like that :
$doctrine = $this->container->get('doctrine');
$importer = Importer::getInstance(Importer::NW, $doctrine);
$importer->import($nid);
I'm starting a new bundle. Its goal is to display some statistics arrays and charts. The problem is I don' t know where to transform raw data into usable data in view's arrays and charts. I read lot of articles about keeping the controllers as thin as possible. And as far as I know, repositories are meant to extract data, not transform them.
Where am I supposed to transform my raw data, according to Symfony2 best practices?
it depends on your application but based on what you described looks like you need to define a Service and write all your logic there so your controller would look something like this
$customService = $this->get('my_custom_service');
$data = $customService->loadMyData();
read more about Services in Symfony: http://symfony.com/doc/current/book/service_container.html
Simply create your own, custom service that uses some repository/ies to extract the data and transform it into usable form.
Sample:
// repository
interface MyRepository {
public function findBySomething($something);
}
class MyRepositoryImpl extends EntityRepository implements MyRepository {
public function findBySomething($something) {
return $this->createQueryBuilder('a')
->where('a.sth = :sth')
->setParameter('std', $something)
->getQuery()
->getResult();
}
}
// service
interface MyService {
public function fetchSomeData();
}
class MyServiceImpl implements MyService {
/** #var MyRespostiory */
private $repo;
public function __construct(MyRepository $repo) {
$this->repo = $repo;
}
public function fetchSomeData() {
$rawData = $this->repo->findBySomething(123);
$data = [];
// do sth
return $data;
}
}
// final usage, eg. within a constructor
class MyConstructor extends Controller {
/** #var MyService */
private $myService;
public function __construct(MyService $myService) {
$this->myService = $myService;
}
public function someAction() {
// you could also get access to the service using $this->get('...')
$data = $this->myService->fetchSomeData();
return $this->render('SomeTemplate', [
'data' => $data
]);
}
}
// service declaration
<service id="myService" class="MyServiceImpl">
<argument type="service" id="doctrine.repository.my_repository" />
</service>