Symfony authenticate with local sql or ldap - symfony

I am trying to configure my authentication with the new security system.
I would like the password verification to be done on a local sql or ldap.
With my current config, the check is done on the local sql, then on the ldap
how to make it one or the other ?
I have been going around in circles for several days...
My security.php
$containerConfigurator->extension('security', [
'providers' => [
'mercredi_user_provider' => [
'entity' => [
'class' => User::class,
'property' => 'username',
],
],
'ville_ldap' => [
'ldap' => [
'service' => 'Symfony\Component\Ldap\Ldap',
'base_dn' => '%env(ACLDAP_DN)%',
'search_dn' => '%env(ACLDAP_USER)%',
'search_password' => '%env(ACLDAP_PASSWORD)%',
'default_roles' => 'ROLE_BOTTIN_ADMIN',
'uid_key' => 'sAMAccountName',
'extra_fields' => ['mail'],
],
],
'all_users' => [
'chain' => [
'providers' => ['ville_ldap', 'mercredi_user_provider'],
],
],
],
]
);
$containerConfigurator->extension(
'security',
[
'firewalls' => [
'main' => [
'provider' => 'all_users',
'custom_authenticator' => MercrediAuthenticator::class,
'form_login_ldap' => [
'service' => 'Symfony\Component\Ldap\Ldap',
'search_dn' => '%env(ACLDAP_USER)%',
'search_password' => '%env(ACLDAP_PASSWORD)%',
'query_string' => '(&(|(sAMAccountName={username}))(objectClass=person))',
'dn_string' => '%env(ACLDAP_DN)%',
'check_path' => 'app_login',
'username_parameter' => 'username',
'password_parameter' => 'password',
],
'logout' => ['path' => 'app_logout'],
],
],
]
);
My Authenticator
public function authenticate(Request $request): PassportInterface
{
$email = $request->request->get('username', '');
$password = $request->request->get('password', '');
$token = $request->request->get('_csrf_token', '');
$request->getSession()->set(Security::LAST_USERNAME, $email);
$badges =
[
new CsrfTokenBadge('authenticate', $token),
new PasswordUpgradeBadge($password, $this->userRepository),
new LdapBadge(LdapMercredi::class, $email),
];
return new Passport(
new UserBadge($email),
new PasswordCredentials($password), $badges
);
}
My class ldap
class LdapMercredi implements LdapInterface
{
private Ldap $ldap;
private string $dn;
private string $user;
private string $password;
public function __construct(string $host, string $dn, string $user, string $password)
{
$this->ldap = Ldap::create(
'ext_ldap',
[
'host' => $host,
'encryption' => 'ssl',
]
);
$this->user = $user;
$this->password = $password;
$this->dn = $dn;
}
public function getEntry(string $uid): ?Entry
{
$this->ldap->bind($this->user, $this->password);
$filter = "(&(|(sAMAccountName=*$uid*))(objectClass=person))";
$query = $this->ldap->query($this->dn, $filter, ['maxItems' => 1]);
$results = $query->execute();
if ($results->count() > 0) {
return $results[0];
}
return null;
}
public function getEntryManager(): EntryManagerInterface
{
return $this->ldap->getEntryManager();
}
public function bind(string $dn = null, string $password = null)
{
dd($dn);
// TODO: Implement bind() method.
}
public function query(string $dn, string $query, array $options = [])
{
dd($query);
// TODO: Implement query() method.
}
public function escape(string $subject, string $ignore = '', int $flags = 0)
{
// TODO: Implement escape() method.
}
}

UPDATE 23/08/21
Here is a configuration that works, there are some points that could improve.
For example, for the LdapBadge badge ('ldapServiceId') you have to give an ldap service, there is indeed the Symfony\Component\Ldap\Ldap which could be suitable but even if I add a tag 'ldap' in services.php
//services.php
$services->set(Ldap::class)->args(['#Symfony\Component\Ldap\Adapter\ExtLdap\Adapter'])->tag('ldap');
I have the error message: "Cannot check credentials using the "Symfony\Component\Ldap\Ldap" ldap service, as such service is not found."
So I copied and pasted the code from Ldap.php to LdapMercredi and declare as service ldap:
In my services.php,
if (interface_exists(LdapInterface::class)) {
$services
->set(Symfony\Component\Ldap\Ldap::class)
->args(['#Symfony\Component\Ldap\Adapter\ExtLdap\Adapter'])
->tag('ldap');
$services->set(Adapter::class)->args([
'$arguments' => [
'$host' => '%env(ACLDAP_URL)%',
'$port' => 636,
'$encryption' => 'ssl',
'$options' => [
'$protocole_version' => 3,
'$referrals' => false,
],
],
]);
$services->set(LdapMercredi::class)
->arg('$adapter', service('Symfony\Component\Ldap\Adapter\ExtLdap\Adapter'))
->tag('ldap');
/**
* Copy/Paste
* #see Ldap
*/
class LdapMercredi implements LdapInterface
{
private $adapter;
public function __construct(AdapterInterface $adapter)
{
$this->adapter = $adapter;
}
/**
* {#inheritdoc}
*/
public function bind(string $dn = null, string $password = null)
{
$this->adapter->getConnection()->bind($dn, $password);
}
/**
* {#inheritdoc}
*/
public function query(string $dn, string $query, array $options = []): QueryInterface
{
return $this->adapter->createQuery($dn, $query, $options);
}
/**
* {#inheritdoc}
*/
public function getEntryManager(): EntryManagerInterface
{
return $this->adapter->getEntryManager();
}
/**
* {#inheritdoc}
*/
public function escape(string $subject, string $ignore = '', int $flags = 0): string
{
return $this->adapter->escape($subject, $ignore, $flags);
}
/**
* Creates a new Ldap instance.
*
* #param string $adapter The adapter name
* #param array $config The adapter's configuration
*
* #return static
*/
public static function create(string $adapter, array $config = []): self
{
if ('ext_ldap' !== $adapter) {
throw new DriverNotFoundException(
sprintf('Adapter "%s" not found. Only "ext_ldap" is supported at the moment.', $adapter)
);
}
return new self(new Adapter($config));
}
}
In security.php:
$containerConfigurator->extension('security', [
'encoders' => [
User::class => ['algorithm' => 'auto'],
],
]);
$containerConfigurator->extension('security', [
'providers' => [
'mercredi_user_provider' => [
'entity' => [
'class' => User::class,
'property' => 'username',
],
],
],
]
);
$authenticators = [MercrediAuthenticator::class];
$main = [
'provider' => 'mercredi_user_provider',
'logout' => ['path' => 'app_logout'],
'form_login' => [],
'entry_point' => MercrediAuthenticator::class,
];
if (interface_exists(LdapInterface::class)) {
$authenticators[] = MercrediLdapAuthenticator::class;
$main['form_login_ldap'] = [
'service' => 'Symfony\Component\Ldap\Ldap',
'check_path' => 'app_login',
];
}
$main['custom_authenticator'] = $authenticators;
$containerConfigurator->extension(
'security',
[
'firewalls' => [
'main' => $main,
],
]
);
MercrediAuthenticator.php
class MercrediAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface, InteractiveAuthenticatorInterface
{
use TargetPathTrait;
public const LOGIN_ROUTE = 'app_login';
private UrlGeneratorInterface $urlGenerator;
private UserRepository $userRepository;
private ParameterBagInterface $parameterBag;
public function __construct(
UrlGeneratorInterface $urlGenerator,
UserRepository $userRepository,
ParameterBagInterface $parameterBag
) {
$this->urlGenerator = $urlGenerator;
$this->userRepository = $userRepository;
$this->parameterBag = $parameterBag;
}
public function supports(Request $request): bool
{
return $request->isMethod('POST') && $this->getLoginUrl($request) === $request->getPathInfo();
}
public function authenticate(Request $request): PassportInterface
{
$email = $request->request->get('username', '');
$password = $request->request->get('password', '');
$token = $request->request->get('_csrf_token', '');
$request->getSession()->set(Security::LAST_USERNAME, $email);
$badges =
[
new CsrfTokenBadge('authenticate', $token),
new PasswordUpgradeBadge($password, $this->userRepository),
];
return new Passport(
new UserBadge($email),
new PasswordCredentials($password), $badges
);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) {
return new RedirectResponse($targetPath);
}
return new RedirectResponse($this->urlGenerator->generate('mercredi_front_profile_redirect'));
}
protected function getLoginUrl(Request $request): string
{
return $this->urlGenerator->generate(self::LOGIN_ROUTE);
}
/**
* Override to change what happens after a bad username/password is submitted.
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
if ($request->hasSession()) {
$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
}
if (interface_exists(LdapInterface::class)) {
return null;
}
$url = $this->getLoginUrl($request);
return new RedirectResponse($url);
}
/**
* Override to control what happens when the user hits a secure page
* but isn't logged in yet.
*/
public function start(Request $request, AuthenticationException $authException = null): Response
{
$url = $this->getLoginUrl($request);
return new RedirectResponse($url);
}
public function isInteractive(): bool
{
return true;
}
The trick of authenticating with ldap and local sql is that the credentials verification priority is higher
Symfony\Component\Ldap\Security\CheckLdapCredentialsListener
class MercrediLdapAuthenticator extends AbstractLoginFormAuthenticator
{
use TargetPathTrait;
public const LOGIN_ROUTE = 'app_login';
private UrlGeneratorInterface $urlGenerator;
private UserRepository $userRepository;
private ParameterBagInterface $parameterBag;
public function __construct(
UrlGeneratorInterface $urlGenerator,
UserRepository $userRepository,
ParameterBagInterface $parameterBag
) {
$this->urlGenerator = $urlGenerator;
$this->userRepository = $userRepository;
$this->parameterBag = $parameterBag;
}
public function authenticate(Request $request): PassportInterface
{
$email = $request->request->get('username', '');
$password = $request->request->get('password', '');
$token = $request->request->get('_csrf_token', '');
$request->getSession()->set(Security::LAST_USERNAME, $email);
$badges =
[
new CsrfTokenBadge('authenticate', $token),
new PasswordUpgradeBadge($password, $this->userRepository),//SelfValidatingPassport?
];
$query = "(&(|(sAMAccountName=*$email*))(objectClass=person))";
$badges[] = new LdapBadge(
LdapMercredi::class,
$this->parameterBag->get(Option::LDAP_DN),
$this->parameterBag->get(Option::LDAP_USER),
$this->parameterBag->get(Option::LDAP_PASSWORD),
$query
);
return new Passport(
new UserBadge($email),
new PasswordCredentials($password), $badges
);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) {
return new RedirectResponse($targetPath);
}
return new RedirectResponse($this->urlGenerator->generate('mercredi_front_profile_redirect'));
}
protected function getLoginUrl(Request $request): string
{
return $this->urlGenerator->generate(self::LOGIN_ROUTE);
}

Related

Dynamic ChoiceType (select2 + AJAX)

I need a form field to choose from thousands of entities, so a dynamic choice system like select2 (with AJAX) is perfectly suited.
My AJAX endpoint works fine, but the custom form type does not work:
class Select2AjaxDataCategoryType extends AbstractType
{
/**
* #var EntityManagerInterface
*/
private $entityManager;
/**
* #var RouterInterface
*/
private $router;
public function __construct(EntityManagerInterface $entityManager,
RouterInterface $router)
{
$this->entityManager = $entityManager;
$this->router = $router;
}
public function getParent()
{
return ChoiceType::class;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->resetModelTransformers();
$builder->resetViewTransformers();
$builder->addModelTransformer(new CallbackTransformer(
function (?DataCategory $dc) {
dump('model transform is called ' . ($dc ? $dc->getId()->toString() : 'null'));
return $dc ? $dc->getId()->toString() : '';
},
function ($id) : ?DataCategory{
dump('model reversetransform is called ' . $id);
$dc = $this->entityManager->getRepository(DataCategory::class)->find($id);
if($dc === null)
throw new TransformationFailedException("Konnte keine Datenkategorie mit ID $id finden");
return $dc;
}
));
$builder->addViewTransformer(new CallbackTransformer( // Identity !!!
function ($dc) {
dump('view transform is called ' . $dc);
return $dc;
},
function ( $id) {
dump('view reversetransform is called ' . $id);
return $id;
}
));
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) { // makes validation pass
$data = $event->getData();
dump($data); // select2'd id, correct
dump($event->getForm()->getName()); // name of my form field
$event->getForm()->getParent()->add( // so this is lik "overwriting"? Documented nowhere :-/
$event->getForm()->getName(),
ChoiceType::class,
['choices' => [$data => $data]]);
$event->getForm()->getParent()->get($event->getForm()->getName())->setData($data);
});
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setRequired('currentDataCategory');
$resolver->setAllowedTypes('currentDataCategory', [DataCategory::class]);
$resolver->setDefaults([
'attr' => [
'data-ajax' => '1',
'data-ajax-endpoint' => $this->router->generate('data-category-manage-select2')
]
]);
}
}
When using this form type, it seems to work, but finally no entity object is returned, but null. According to symfony debug toolbar however, the value is received:
Also the dumps indicate that the view and model transformers were called:
For the sake of completeness (I hope we'll find a perfect solution and help others), here is my js code (it works):
$('select[data-ajax=1]').select2({
theme: "bootstrap4",
placeholder: "Bitte wählen",
ajax: {
url: function() { return $(this).data('ajax-endpoint');},
dataType: 'json',
data: function (params) {
var query = {
search: params.term,
page: params.page || 0
}
// Query parameters will be ?search=[term]&page=[page]
return query;
}
}
});
I have solved the problem, here is my complete solution:
$('select[data-ajax=1]').select2({
theme: "bootstrap4",
placeholder: "Bitte wählen",
ajax: {
url: function() { return $(this).data('ajax-endpoint');},
dataType: 'json',
data: function (params) {
var query = {
search: params.term,
page: params.page || 0
}
// Query parameters will be ?search=[term]&page=[page]
return query;
}
}
});
The new form type is fixed for one class DataCategory, and works both for single and multiple select's.
I have build-in a distinction between select2 frontend and the standard EntityType (mainly for testing reasons, because the new select2 based approach does not allow PHPUnit tests that use symfony's Client (WebTestCase)): If there are less than 50 DataCategory entities in the DB, the field falls back to EntityType
class Select2AjaxDataCategoryType extends AbstractType
{
/**
* #var EntityManagerInterface
*/
private $entityManager;
/**
* #var RouterInterface
*/
private $router;
private $transformCallback;
public function __construct(EntityManagerInterface $entityManager,
RouterInterface $router)
{
$this->entityManager = $entityManager;
$this->router = $router;
$this->transformCallback = function ($stringOrDc) {
if (is_string($stringOrDc)) return $stringOrDc;
else return $stringOrDc->getId()->toString();
};
}
public function getParent()
{
if($this->entityManager->getRepository(DataCategory::class)->count([]) > 50)
return ChoiceType::class;
else
return EntityType::class;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
if($this->entityManager->getRepository(DataCategory::class)->count([]) > 50) {
$builder->addModelTransformer(new CallbackTransformer(
function ($dc) {
/** #var $dc DataCategory|DataCategory[]|string|string[] */
/** #return string|string[] */
dump('model transform', $dc);
if($dc === null) return '';
if(is_array($dc)) {
return array_map($this->transformCallback, $dc);
} else if($dc instanceof Collection) {
return $dc->map($this->transformCallback);
} else {
return ($this->transformCallback)($dc);
}
},
function ($id) {
dump('model reversetransform', $id);
if (is_string($id)) {
$dc = $this->entityManager->getRepository(DataCategory::class)->find($id);
if ($dc === null)
throw new TransformationFailedException("Konnte keine Datenkategorie mit ID $id finden");
dump($dc);
return $dc;
} else {
$ret = [];
foreach($id as $i){
$dc = $this->entityManager->getRepository(DataCategory::class)->find($i);
if ($dc === null)
throw new TransformationFailedException("Konnte keine Datenkategorie mit ID $id finden");
$ret[] = $dc;
}
return $ret;
}
}
));
$builder->resetViewTransformers();
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$dataId = $event->getData();
dump('presubmit', $dataId, $event->getForm()->getConfig()->getOptions()['choices']);
if(empty($dataId))
return;
$name = $event->getForm()->getName();
if (is_array($dataId)) { // multiple-true-case
if (!empty(array_diff($dataId, $event->getForm()->getConfig()->getOptions()['choices']))) {
$options = $event->getForm()->getParent()->get($name)->getConfig()->getOptions();
$options['choices'] = array_combine($dataId, $dataId);
$event->getForm()->getParent()->add($name, Select2AjaxDataCategoryType::class, $options);
$event->getForm()->getParent()->get($name)->submit($dataId);
$event->stopPropagation();
}
} else { // multiple-false-case
if($dataId instanceof DataCategory){
$dataId = $dataId->getId()->toString();
throw new \Exception('Hätte ich nicht erwartet, sollte string sein');
}
if (!in_array($dataId, $event->getForm()->getConfig()->getOptions()['choices'])) {
$options = $event->getForm()->getParent()->get($name)->getConfig()->getOptions();
$options['choices'] = [$dataId => $dataId];
$event->getForm()->getParent()->add($name, Select2AjaxDataCategoryType::class, $options);
$event->getForm()->getParent()->get($name)->submit($dataId);
$event->stopPropagation();
}
}
});
// $builder->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event){
// dump("pre set data", $event->getData());
// });
} else {
}
}
public function configureOptions(OptionsResolver $resolver)
{
if($this->entityManager->getRepository(DataCategory::class)->count([]) > 50) {
$resolver->setDefaults([
'attr' => [
'data-ajax' => '1',
'data-ajax-endpoint' => $this->router->generate('data-category-manage-select2')
],
'choices' => function (Options $options) {
$data = $options['data'];
dump('data', $data);
if($data !== null) {
if(is_array($data) || $data instanceof Collection){
$ret = [];
foreach ($data as $d) {
$ret[$d->description . ' (' . $d->name . ')'] = $d->getId()->toString();
}
dump($ret);
return $ret;
} else if ($data instanceof DataCategory){
return [$data->description . ' (' . $data->name . ')' => $data->getId()->toString()];
} else {
throw new \InvalidArgumentException("Argument unerwartet.");
}
} else {
return [];
}
}
]);
} else {
$resolver->setDefaults([
'class' => DataCategory::class,
'choice_label' => function ($cat, $key, $index) { return DataCategory::choiceLabel($cat);},
'choices' => function (Options $options) {
return $this->entityManager->getRepository(DataCategory::class)->getValidChildCategoryChoices($options['currentDataCategory']);
}
]);
}
}
}
It is very important to set the 'data' option when using this new type, otherwise the choices option is not correctly set:
$builder->add('summands', Select2AjaxDataCategoryType::class,[
'currentDataCategory' => $mdc,
'data' => $mdc->summands->toArray(),
'multiple' => true,
'required' => false,
'label' => 'Summierte Kategorien',
]);

symfony 4 - Redirect after form select

I have a problem: I can't redirect after submitting a form with select element.
I have to select a duty (permanence), and once selected & submitted, I have to redirect to its page. Redirect does not happen, below is the controller.
/**
* #Route("/", name="home")
* #Route("/permanences/{id}", name="perm_show")
*/
public function home(Request $request)
{
$form = $this->createForm(SelectPermanenceType::class, $permanence = null);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid())
{
$permanence = $form['nom']->getData();
}
var_dump($permanence );
return $this->render('webclient/home.html.twig', [
'SelectPermanence' => $form->createView()
]);
}
/**
* #Route("/permanences", name="permanences")
*/
public function index(PermanenceRepository $repo)
{
$permanences = $repo->findAll();
return $this->render('webclient/permanences.html.twig', [
'controller_name' => 'WebclientController',
'permanences' => $permanences
]);
}
/**
* #Route("/permanences/new", name="perm_new")
* #Route("/permanences/{id}/edit", name="perm_edit")
*/
public function form(Permanence $permanence = null, Request $request, ObjectManager $manager)
{
if(!$permanence){
$permanence = new Permanence();
}
$form = $this->createForm(PermanenceType::class, $permanence);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid())
{
if(!$permanence->getId()){
$permanence->setCreatedAt(new \DateTime());
}
$manager->persist($permanence);
$manager->flush();
return $this->redirectToRoute('perm_show', ['id' => $permanence->getId()]);
}
return $this->render('webclient/new_perm.html.twig', [
'newPermanence' => $form->createView(),
'editMode' => $permanence->getId() !== null
]);
}
/**
* #Route("/permanences/{id}", name="perm_show")
*/
public function show(Permanence $permanence)
{
return $this->render('webclient/show.html.twig',[
'permanence' => $permanence
]);
}
The page is correctly displayed, the dowpdown as well, but when I select and send, nothings happens.
Has anyone an idea ?
You have to change the order of presence of your routes. They would better have this order:
home
permanences
show
new & edit
As below:
/**
* #Route("/", name="home")
*/
public function home(Request $request)
{
$form = $this->createForm(SelectPermanenceType::class, $permanence = null);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid())
{
$permanence = $form['nom']->getData();
}
var_dump($permanence );
return $this->render('webclient/home.html.twig', [
'SelectPermanence' => $form->createView()
]);
}
/**
* #Route("/permanences", name="permanences")
*/
public function index(PermanenceRepository $repo)
{
$permanences = $repo->findAll();
return $this->render('webclient/permanences.html.twig', [
'controller_name' => 'WebclientController',
'permanences' => $permanences
]);
}
/**
* #Route("/permanences/{id}", name="perm_show")
*/
public function show(Permanence $permanence)
{
return $this->render('webclient/show.html.twig',[
'permanence' => $permanence
]);
}
/**
* #Route("/permanences/new", name="perm_new")
* #Route("/permanences/{id}/edit", name="perm_edit")
*/
public function form(Permanence $permanence = null, Request $request, ObjectManager $manager)
{
if(!$permanence){
$permanence = new Permanence();
}
$form = $this->createForm(PermanenceType::class, $permanence);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid())
{
if(!$permanence->getId()){
$permanence->setCreatedAt(new \DateTime());
}
$manager->persist($permanence);
$manager->flush();
return $this->redirectToRoute('perm_show', ['id' => $permanence->getId()]);
}
return $this->render('webclient/new_perm.html.twig', [
'newPermanence' => $form->createView(),
'editMode' => $permanence->getId() !== null
]);
}
Also I remove the duplicate route definition for perm_show, at each route should only be defined once.

__construct() must be an instance of Post\Model\PostTable

Try add new module? but got are error:
Argument 1 passed to Post\Controller\PostController::__construct()
must be an instance of Post\Model\PostTable, none given, called in
W:\domains\zend_blog\skeleton-application\vendor\zendframework\zend-servicemanager\src\Factory\InvokableFactory.php
namespace Post\Controller;
use Post\Model\Post;
// Add the following import:
use Post\Model\PostTable;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
class PostController extends AbstractActionController
{
private $table;
/**
* Execute the request
*
* #param MvcEvent $e
* #return mixed
*/
// Add this constructor:
public function __construct(PostTable $table)
{
$this->table = $table;
}
public function indexAction()
{
return new ViewModel([
'post' => $this->table->fetchAll()
]);
}
}
Post Table:
<?php
namespace Post\Model;
use RuntimeException;
use Zend\Db\TableGateway\TableGatewayInterface;
use Zend\Db\TableGateway\TableGateway;
class PostTable
{
private $tableGateway;
public function __construct(TableGateway $tableGateway)
{
// $this->tableGateway = $tableGateway;
$this->tableGateway = $tableGateway;
}
public function fetchAll()
{
return $this->tableGateway->select();
}
public function getPost($id)
{
$id = (int) $id;
$rowset = $this->tableGateway->select(['id' => $id]);
$row = $rowset->current();
if (! $row) {
throw new RuntimeException(sprintf(
'Could not find row with identifier %d',
$id
));
}
return $row;
}
public function savePodt(Post $album)
{
$data = [
'artist' => $album->artist,
'title' => $album->title,
];
$id = (int) $album->id;
if ($id === 0) {
$this->tableGateway->insert($data);
return;
}
if (! $this->getPost($id)) {
throw new RuntimeException(sprintf(
'Cannot update album with identifier %d; does not exist',
$id
));
}
$this->tableGateway->update($data, ['id' => $id]);
}
public function deletePost($id)
{
$this->tableGateway->delete(['id' => (int) $id]);
}
}
Module:
namespace Post;
use Zend\ModuleManager\Feature\ConfigProviderInterface;
use Zend\ModuleManager\Feature\ServiceProviderInterface;
class Module implements ConfigProviderInterface,ServiceProviderInterface
{
public function getConfig()
{
return include __DIR__ . '/../config/module.config.php';
}
/**
* Expected to return \Zend\ServiceManager\Config object or array to
* seed such an object.
*
* #return array|\Zend\ServiceManager\Config
*/
public function getServiceConfig()
{
return [
'factories' => [
Model\PostTable::class => function($container) {
$tableGateway = $container->get(Model\PostTableGateway::class);
return new Model\PostTable($tableGateway);
},
Model\PostTableGateway::class => function ($container) {
$dbAdapter = $container->get(AdapterInterface::class);
$resultSetPrototype = new ResultSet();
$resultSetPrototype->setArrayObjectPrototype(new Model\Post());
return new TableGateway('post', $dbAdapter, null, $resultSetPrototype);
},
],
];
}
public function getControllerConfig() {
return [
'factories' => [
Controller\PostController::class => function($container) {
return new Controller\PostController(
$container->get(Model\PostTable::class)
);
},
],
];
}
}
Most probably you have a duplicate setting for the controller factory in the module.config.php. Check:
'controllers' => [
'factories' => [
Controller\PostController::class => InvokableFactory::class,
...
],
],
Remove line:
Controller\PostController::class => InvokableFactory::class,
so that the factory method in your Module.php is used.

Symfony2 Dynamic Form Modification not saving generated data

I'm going crazy because if I choose a client from an entity field, it correctly populate the second entity field called proposals. Then I choose the proposal dynamically generated, but when I save the form it saves the form correctly but without filling the proposal field. I followed the Symfony Tutorial about the Dynamic Forms which can be found here
This is my FormType code:
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('client', 'entity', array(
'class' => 'AppBundle\Entity\Client',
'property' => 'name',
'label' => 'Client:',
'empty_value' => '',
'required' => false,
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('u')
->orderBy('u.name', 'ASC');
},
));
$formModifier = function (FormInterface $form, Client $client = null) {
$proposals = null === $client ? array() : $this->em->getRepository('AppBundle:Proposals')->findBy(
array('client'=>$client->getId()),
array('id' => 'DESC'));
$form->add('proposal', 'entity', array(
'class' => 'AppBundle\Entity\Proposal',
'choice_label' => 'subject',
'placeholder' => '',
'choices' => $proposals,
'label' => 'Proposal',
'required' => false
));
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formModifier) {
$client = null;
$data = $event->getData();
if(!empty($data)) {
$client = $data->getClient();
}
$formModifier($event->getForm(), $client );
}
);
$builder->get('client')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) use ($formModifier) {
$client = $event->getForm()->getData();
$formModifier($event->getForm()->getParent(), $client);
}
);
This is the Prenotazione Entity, the one who belong the form.
class Prenotazione {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="Client", inversedBy="prenotazioni")
* #ORM\JoinColumn(name="client_id", referencedColumnName="id")
*/
private $client;
/**
* #ORM\OneToOne(targetEntity="Proposal", inversedBy="prenotazione")
* #ORM\JoinColumn(name="proposal_id", referencedColumnName="id")
*/
private $proposal;
public function getId() {
return $this->id;
}
public function setProposal(\AppBundle\Entity\Proposal $proposal = null)
{
$this->proposal = $proposal;
return $this;
}
public function getProposal() {
return $this->proposal;
}
public function setClient(\AppBundle\Entity\Client $client = null)
{
$this->client = $client;
return $this;
}
public function getClient()
{
return $this->client;
}
}
Where am I wrong ?
Are you sure your proposals query is correct?
$proposals = null === $client ? array() : $this->em->getRepository('AppBundle:Proposals')->findBy(
array('client'=>$client->getId()),
array('id' => 'DESC'));
Shouldn't this be either array('client_id' => $client->getId()), or array('client' => $client),?
Try checking the actual content of $proposals by adding a dump($proposals) just below and looking up the result in the symfony profiler.

jsmPayment etsPaymentOgone gives me an error The controller must return a response

I'm trying to implement JSMPayment and EtsPaymentOgoneBundle without success.
I get the error : "The controller must return a response". I'm agree with that but it's so written in the documentation. So am I something wrong or is it a bug/error in the documentation.
The error may be this but it's so written in doc...
return array(
'form' => $form->createView()
);
Now, if I change this line and return to a twig template, I only get one radio button. Why ?
Any help will very help me because, I'm really lost.
My all controller
/**
*
*/
class PaymentController extends Controller
{
/** #DI\Inject */
private $request;
/** #DI\Inject */
private $router;
/** #DI\Inject("doctrine.orm.entity_manager") */
private $em;
/** #DI\Inject("payment.plugin_controller") */
private $ppc;
/**
*
* #param \CTC\Bundle\OrderBundle\Controller\Order $order
* #return RedirectResponse
*/
public function detailsAction(Order $order, Request $request)
{
$form = $this->getFormFactory()->create('jms_choose_payment_method', null, array(
'amount' => $order->getPackage()->getAmount(),
'currency' => 'EUR',
'default_method' => 'ogone_gateway', // Optional
'predefined_data' => array(
'ogone_gateway' => array(
'tp' => '', // Optional
'PM' => $pm, // Optional - Example value: "CreditCard" - Note: You can consult the list of PM values on Ogone documentation
'BRAND' => $brand, // Optional - Example value: "VISA" - Note: If you send the BRAND field without sending a value in the PM field (‘CreditCard’ or ‘Purchasing Card’), the BRAND value will not be taken into account.
'CN' => $billingAddress->getFullName(), // Optional
'EMAIL' => $this->getUser()->getEmail(), // Optional
'OWNERZIP' => $billingAddress->getPostalCode(), // Optional
'OWNERADDRESS' => $billingAddress->getStreetLine(), // Optional
'OWNERCTY' => $billingAddress->getCountry()->getName(), // Optional
'OWNERTOWN' => $billingAddress->getCity(), // Optional
'OWNERTELNO' => $billingAddress->getPhoneNumber(), // Optional
'lang' => $request->getLocale(), // 5 characters maximum, for e.g: fr_FR
'ORDERID' => '123456', // Optional, 30 characters maximum
),
),
));
if ('POST' === $this->request->getMethod()) {
$form->bindRequest($this->request);
if ($form->isValid()) {
$this->ppc->createPaymentInstruction($instruction = $form->getData());
$order->setPaymentInstruction($instruction);
$this->em->persist($order);
$this->em->flush($order);
return new RedirectResponse($this->router->generate('payment_complete', array(
'orderNumber' => $order->getOrderNumber(),
)));
}
}
return array(
'form' => $form->createView()
);
}
/**
*
*/
public function completeAction(Order $order)
{
$instruction = $order->getPaymentInstruction();
if (null === $pendingTransaction = $instruction->getPendingTransaction()) {
$payment = $this->ppc->createPayment($instruction->getId(), $instruction->getAmount() - $instruction->getDepositedAmount());
} else {
$payment = $pendingTransaction->getPayment();
}
$result = $this->ppc->approveAndDeposit($payment->getId(), $payment->getTargetAmount());
if (Result::STATUS_PENDING === $result->getStatus()) {
$ex = $result->getPluginException();
if ($ex instanceof ActionRequiredException) {
$action = $ex->getAction();
if ($action instanceof VisitUrl) {
return new RedirectResponse($action->getUrl());
}
throw $ex;
}
} else if (Result::STATUS_SUCCESS !== $result->getStatus()) {
throw new \RuntimeException('Transaction was not successful: '.$result->getReasonCode());
}
// payment was successful, do something interesting with the order
}
public function cancelAction(Order $order)
{
die('cancel the payment');
}
/** #DI\LookupMethod("form.factory") */
protected function getFormFactory() { }
}
if you use
return array(
'form' => $form->createView()
);
at the controller, then you should add #Template annotation to the controller action
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
class PaymentController extends Controller
{
...
/**
*
* #param \CTC\Bundle\OrderBundle\Controller\Order $order
* #Template()
* #return RedirectResponse
*/
public function detailsAction(Order $order, Request $request)
or you should return "render" with a template
return $this->render('MyAppSomeBundle:Payment:details.html.twig', array( 'form' => $form->createView());

Resources