My validator looks like:
<?php
declare(strict_types=1);
namespace App\Infrastructure\Domain\Model\Company\Validator;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\UnexpectedValueException;
use Symfony\Contracts\Translation\TranslatorInterface;
final class ContainsUnsupportedSymbolsValidator extends ConstraintValidator
{
public function __construct(
private TranslatorInterface $translator
)
{
}
public function validate($value, Constraint $constraint): void
{
if (!$constraint instanceof ContainsUnsupportedSymbols) {
throw new UnexpectedTypeException($constraint, ContainsUnsupportedSymbols::class);
}
// custom constraints should ignore null and empty values to allow
// other constraints (NotBlank, NotNull, etc.) to take care of that
if (null === $value || '' === $value) {
return;
}
if (!is_string($value)) {
// throw this exception if your validator cannot handle the passed type so that it can be marked as invalid
throw new UnexpectedValueException($value, 'string');
}
if (!preg_match('/^\+?[0-9]{3}-?[0-9]{6,12}$/', $value, $matches)) {
// the argument must be a string or an object implementing __toString()
$this->context->buildViolation(
$this->translator->trans($constraint->message, ['{phone}' => $value])
)->addViolation();
}
}
}
The test I wrote for valid case is:
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Infrastructure\Domain\Model\Company\Validator;
use App\Infrastructure\Domain\Model\Company\Validator\ContainsUnsupportedSymbols;
use App\Infrastructure\Domain\Model\Company\Validator\ContainsUnsupportedSymbolsValidator;
use App\Tests\Unit\UnitTestCase;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
final class ContainsUnsupportedSymbolsValidatorTest extends UnitTestCase
{
public function testValidatorWithValidPhone():void
{
$phone = "+359883202020";
$translationMock = $this->createMock(TranslatorInterface::class);
$constraint = new ContainsUnsupportedSymbols();
$constraintValidator = new ContainsUnsupportedSymbolsValidator($translationMock);
$this->assertEmpty($constraintValidator->validate($phone, $constraint));
}
}
Is it correct to test with assertEmpty when the validate method return type is void?
And how to test with incorrect data? Tried to mock the context and both buildViolation and addViolation methods but got error:
Error: Call to a member function buildViolation() on null
This is what I tried with incorrect phone
public function testValidatorWithInvalidPhone():void
{
$phone = "+359()883202020";
$translationMock = $this->createMock(TranslatorInterface::class);
$contextMock = $this->createMock(ExecutionContextInterface::class);
$contextMock->expects($this->once())
->method('buildViolation')
->with('message');
$contextMock->expects($this->once())
->method('addViolation');
$constraint = new ContainsUnsupportedSymbols();
$constraintValidator = new ContainsUnsupportedSymbolsValidator($translationMock);
$this->assertEmpty($constraintValidator->validate($phone, $constraint));
}
Your context is null. You have to initialize the ConstraintValidator extended by your custom validator.
The question has found an answer here: Unit testing a custom symfony constraint
In short here is an example https://github.com/symfony/symfony/blob/6.2/src/Symfony/Component/Validator/Tests/Constraints/BlankValidatorTest.php
In the example the test class extends ConstraintValidatorTestCase that defines a context (look here: https://github.com/symfony/validator/blob/6.1/Test/ConstraintValidatorTestCase.php)
Related
API Platform version(s) affected: 2.6.8
Description
In a project which uses PostgreSQL and API-Platform, I need to filter all records by a locale string. A doctrine filter is my preferred choice to do so.
This is the filter:
class LocaleFilter extends SQLFilter
{
public const LOCALE_FILTER_NAME = 'locale_filter';
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
{
if (!$targetEntity->reflClass->implementsInterface(LocalizedEntityInterface::class)) {
return '';
}
return $targetTableAlias . '.locale = ' . $this->getParameter('locale');
}
}
The parameter locale will be set on each onKernelRequest event, the locale is the value of the header X-Locale:
public function onKernelRequest(RequestEvent $event): void
{
$locale = $event->getRequest()->headers->get('X-Locale');
$this->setFilterLocale($locale);
}
private function setFilterLocale(string $locale): void
{
if (!$this->entityManager->hasFilters()) {
return;
}
$localeFilter = $this->entityManager->getFilters()->getFilter(LocaleFilter::LOCALE_FILTER_NAME);
$localeFilter->setParameter('locale', $locale);
}
Now, when I send a request to a collectionOperations endpoint, such as http://example.com/products with the X-Locale header value de_DE, the filter is working and I get a response which contains only the according data in de_DE. When I send a request with locale fr_FR, I get a response with data in fr_FR.
But, when I send a request with the same X-Locale header to a itemOperations endpoint like http://example.com/products/<a-existing-id> I'm getting the error message The parameter "locale" is not set which comes from doctrine.
After investigating that issue, I can say that it works when I override the default ItemDataProvider from API-platform:
<?php
namespace App\DataProvider;
[...]
class ItemDataProvider implements ItemDataProviderInterface
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly RequestStack $requestStack,
) {
}
public function getItem(string $resourceClass, $id, ?string $operationName = null, array $context = []): object
{
$locale = $this->requestStack->getMainRequest()->headers->get('X-Locale');
if ($this->entityManager->hasFilters()) {
$localeFilter = $this->entityManager->getFilters()->getFilter(LocaleFilter::LOCALE_FILTER_NAME);
$localeFilter->setParameter('locale', $locale);
}
$query = $this->entityManager->getRepository($resourceClass)
->createQueryBuilder('x')
->where('x.publicId = :pubid')
->setParameter('pubid', $id)
->getQuery();
return $query->getOneOrNullResult();
}
}
But is still required to set the filter value again in "my own" ItemDataProvider. If I delete the first 7 lines of the method getItem of the ItemDataProvider, I get the same error from above.
That doesn't make sense like that, does it? It seems like Api-Platform overrides the Doctrine filters in the default ItemDataProvider and make them useless. Howewer, I didn't found the reason for that issue.
Overriding the ItemDataProvider is a working workaround, but I don't think it's a good one, since the cause is more likely a bug and that way some features of Api-Platform are no longer present in the whole project.
I have an EventSubscriberInterface called PostCreation listening to the POST_WRITE event.
The method called when the event occurs must call a service according to the "type" attribute of the object which the event occured on.
The following code is a working example of what I want:
<?php
class PostCreation implements EventSubscriberInterface
{
private $processAfterPostCreationTypeAService;
private $processAfterPostCreationTypeBService;
public function __construct(
ProcessAfterPostCreationTypeAService $processAfterPostCreationTypeAService,
ProcessAfterPostCreationTypeBService $processAfterPostCreationTypeBService
) {
$this->processAfterPostCreationTypeAService = $processAfterPostCreationTypeAService;
$this->processAfterPostCreationTypeBService = $processAfterPostCreationTypeBService;
}
public static function getSubscribedEvents(): array
{
return [KernelEvents::VIEW => ['processPostCreation', EventPriorities::POST_WRITE]];
}
public function processPostCreation(GetResponseForControllerResultEvent $event): void
{
$myObject = $event->getControllerResult();
if (!$event->getRequest()->isMethod('POST') ||
!$myObject instanceof MyClass
) {
return;
}
/* some code */
if($myObject->getType() === 'A') {
$this->processAfterPostCreationTypeAService->doSomething($myObject);
} else if($myObject->getType() === 'B') {
$this->processAfterPostCreationTypeBService->doSomething($myObject);
}
/* some code */
}
}
Doing it that way is not maintainable so I would like to find another solution, but I can't find it myself so I need help:
The constructor of PostCreation can't know the object.type because it's called before the event occurs
I can't instantiate easily ProcessAfterPostCreationTypeAService or ProcessAfterPostCreationTypeBService in processPostCreation method because they are services as well (and needs dependancy injection, configured to be autowired)
I'm sure dependancy injection could help me, but I can't find how.
So how to do something maintainable?
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;
}
}
I have a boolean variable(0, 1) in my database and I want to filter it to a word 0 for 'NO', and 1 for 'Yes'. how can I do that in a twig template
I want something like {{ bool_var | '??' }} where the '??' is the filter
Quick way to achieve that is to use the ternary operator:
{{ bool_var ? 'Yes':'No' }}
http://twig.sensiolabs.org/doc/templates.html#other-operators
You could also create a custom filter that would do this. Read about custom TWIG extensions - http://symfony.com/doc/current/cookbook/templating/twig_extension.html
To build on what #dmnptr said in his last paragraph, in your app bundle, create a /Twig folder and create an AppExtension class inside.
class AppExtension extends \Twig_Extension
{
public function getFilters()
{
return array(
new \Twig_SimpleFilter('boolean', array($this, 'booleanFilter')),
);
}
public function booleanFilter($value)
{
if ($value) {
return "Yes";
} else {
return "No";
}
}
public function getName()
{
return 'app_extension';
}
}
Then, in your bundle's Resources/config/ folder, add the following to your services.yml where class is the class of the new class:
app.twig_extension:
class: [YourAppBundleNamespace]\Twig\AppExtension
public: false
tags:
- { name: twig.extension }
The filter will be available in Twig by simply appending a |boolean to any variable.
Or even better you could make a boolean to string transformer and add it to your form.
It might be 'more' code but the upside is reusability. You wouldn't have to make your templates dirty with logic and you could reuse it to all the forms you want :)
Pros:
Not tied to the form component so you can still use it.
Use it anywhere, more functionality than a twig extension.
No need to mess with twig or symfony configuration.
Can use it in forms themselves.
Documentation:
http://symfony.com/doc/current/cookbook/form/data_transformers.html
Example from:
Symfony2 Forms BooleanToStringTransformer Issue
<?php
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
class BooleanToStringTransformer implements DataTransformerInterface
{
private $trueValue;
private $falseValue;
public function __construct($trueValue, $falseValue)
{
$this->trueValue = $trueValue;
$this->falseValue = $falseValue;
}
public function transform($value)
{
if (null === $value) {
return null;
}
if (!is_bool($value)) {
throw new TransformationFailedException('Expected a Boolean.');
}
return true === $value ? $this->trueValue : $this->falseValue;
}
public function reverseTransform($value)
{
if (null === $value) {
return null;
}
if (!is_string($value)) {
throw new TransformationFailedException('Expected a string.');
}
return $this->trueValue === $value;
}
}
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.