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

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.

Related

Api Platform 2 - Pass array of options to a custom encoder

I am using API Platform 2.6
As I need to export some contents to PDF, I had to create a custom encoder for that to work.
1st, I registered a new format, so I can request application/pdf content.
#File: config/packages/api_platform:
api_platform:
....
formats:
....
pdf: ['application/pdf']
2nd, Created a PdfEncoder that extends EncoderInterface
#File: src/Encodre/PdfEncoder.php:
class PdfEncoder implements EncoderInterface
{
public const FORMAT = 'pdf';
public function __construct(
private readonly Environment $twig,
private readonly Pdf $pdf,
) {
}
public function encode($data, $format, array $context = []): string
{
$content = $this->twig->render('pdf/template.html.twig', ['data' => $data]);
$filename = sprintf('/tmp/%s.pdf', uniqid());
$this->pdf->generateFromHtml($content, $filename);
return file_get_contents($filename);
}
public function supportsEncoding($format): bool
{
return self::FORMAT === $format;
}
}
3d, on the resource, I created the appropriate call:
#File src/Resource/MyResource.php
#[ApiResource(
itemOperations: [
'export' => [
'method' => 'GET',
'path' => '/myResource/{id}/export',
'requirements' => ['id' => '\d+'],
'output' => MyResourceView::class
],
],
)]
class MyResource
{
public int $id;
/** Other Attributes **/
}
As you can see, On the PdfEncoder class, I hard coded the path to the Twig Template,
But As I need to export other resources to PDF, and they are using different templates, I need to pass this template path as an option to the encoder, Maybe on the context variable would be great.
Here is what I am looking for.
#File: src/Encodre/PdfEncoder.php:
class PdfEncoder implements EncoderInterface
{
....
public function encode($data, $format, array $context = []): string
{
$template = $context['export']['template']?? null;
if (!$template){
throw new \Exception('Twig template is not defined');
}
$content = $this->twig->render($template, ['data' => $data]);
$filename = sprintf('/tmp/%s.pdf', uniqid());
$this->pdf->generateFromHtml($content, $filename);
return file_get_contents($filename);
}
...
}
Is there a way to accomplish this?
I tried adding that on the Resource, but ApiPlatform deleted them before having them on the context array
#File src/Resource/MyResource.php
#[ApiResource(
itemOperations: [
'export' => [
'method' => 'GET',
'path' => '/myResource/{id}/export',
'requirements' => ['id' => '\d+'],
'output' => MyResourceView::class,
'export' => [
'template' => 'pdf/template.html.twig',
]
],
],
)]
class MyResource
{
public int $id;
/** Other Attributes **/
}
This is what I've come up with so far,
This helps me to get the template name dynamically.
#File: src/Encodre/PdfEncoder.php:
class PdfEncoder implements EncoderInterface
{
public const FORMAT = 'pdf';
public function __construct(
private readonly Environment $twig,
private readonly Pdf $pdf,
) {
}
public function encode($data, $format, array $context = []): string
{
$template = sprintf('pdf/%s.html.twig', lcfirst($context['output']['name']));
if (!$this->twig->getLoader()->exists($template)) {
throw new \RuntimeException(sprintf('Missing template for %s', $context['output']['class']));
}
$content = $this->twig->render($template, ['data' => $data]);
$filename = sprintf('/tmp/%s.pdf', uniqid());
$this->pdf->generateFromHtml($content, $filename);
return file_get_contents($filename);
}
public function supportsEncoding($format): bool
{
return self::FORMAT === $format;
}
}
$context['output']['class'] is the name of the View class (base name)
So this way, for every View, I have a twig file.
I won't mark this answer as the solution as I think there might be nicer/recommended way to do that.

Symfony authenticate with local sql or ldap

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);
}

PDF document creation EasyAdmin symfony 5

I have this configuration which allows me to create a pdf document in the CRUD, is there a way to add this code in the CRUD easyAdmin or link the CRUD of my EasyAdmin documentos to the CRUD of symfony.
I have problems creating the document in the EasyAdmin table
DocumentController.php
<?php
namespace App\Controller;
use App\Entity\Document;
use App\Form\DocumentType;
use App\Repository\DocumentRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use App\Service\FileUploader;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
/**
* #IsGranted("ROLE_USER")
* #Route("/documents")
*/
class DocumentController extends AbstractController
{
/**
* #Route("/", name="document_index", methods={"GET"})
*/
public function index(DocumentRepository $documentRepository): Response
{
return $this->render('document/index.html.twig', [
'documents' => $documentRepository->findAll([], ['created_at' => 'desc']),
]);
}
/**
* #Route("/new", name="document_new", methods={"GET","POST"})
*/
public function new(Request $request, FileUploader $fileUploader): Response
{
$document = new Document();
$document->setCreatedAt(new \DateTime('now'));
$form = $this->createForm(DocumentType::class, $document);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$entityManager = $this->getDoctrine()->getManager();
$file = $form['fileDocument']->getData();
$originalFilename = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
// this is needed to safely include the file name as part of the URL
$fileName = transliterator_transliterate('Any-Latin; Latin-ASCII; [^A-Za-z0-9_] remove; Lower()', $originalFilename);
$fileName = md5(uniqid()) . '.' . $file->guessExtension();
$file->move(
$this->getParameter('brochures_directory'),
$fileName
);
$document->setFileDocument($fileName);
$entityManager->persist($document);
$entityManager->flush();
return $this->redirectToRoute('document_index', array('id' => $document->getId()));
}
return $this->render('document/new.html.twig', [
// 'document' => $document,
'form' => $form->createView(),
]);
}
/**
* #Route("/{id}", name="document_show", methods={"GET"})
*/
public function show(Document $document): Response
{
return $this->render('document/show.html.twig', [
'document' => $document,
]);
}
/**
* #Route("/{id}/edit", name="document_edit", methods={"GET","POST"})
*/
public function edit(Request $request, Document $document): Response
{
$form = $this->createForm(DocumentType::class, $document);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$file = $form['fileDocument']->getData();
$originalFilename = pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
// this is needed to safely include the file name as part of the URL
$fileName = transliterator_transliterate('Any-Latin; Latin-ASCII; [^A-Za-z0-9_] remove; Lower()', $originalFilename);
$fileName = md5(uniqid()) . '.' . $file->guessExtension();
$file->move(
$this->getParameter('brochures_directory'),
$fileName
);
$document->setFileDocument($fileName);
$this->getDoctrine()->getManager()->flush();
return $this->redirectToRoute('document_index');
}
return $this->render('document/edit.html.twig', [
'document' => $document,
'form' => $form->createView(),
]);
}
/**
* #Route("/{id}", name="document_delete", methods={"DELETE"})
*/
public function delete(Request $request, Document $document): Response
{
if ($this->isCsrfTokenValid('delete' . $document->getId(), $request->request->get('_token'))) {
$entityManager = $this->getDoctrine()->getManager();
$entityManager->remove($document);
$entityManager->flush();
}
return $this->redirectToRoute('document_index');
}
}
DocumentCrudController Easy Admin
<?php
namespace App\Controller\Admin;
use App\Entity\Document;
use App\Entity\Publication;
use EasyCorp\Bundle\EasyAdminBundle\Config\Action;
use EasyCorp\Bundle\EasyAdminBundle\Config\Actions;
use EasyCorp\Bundle\EasyAdminBundle\Config\Crud;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField;
use EasyCorp\Bundle\EasyAdminBundle\Field\IdField;
use EasyCorp\Bundle\EasyAdminBundle\Field\ImageField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextareaField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Validator\Constraints\File;
class DocumentCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return Document::class;
}
public function configureCrud(Crud $crud): Crud
{
return $crud
->setPageTitle(Crud::PAGE_INDEX, 'Liste de Documents')
;
}
public function configureFields(string $pageName): iterable
{
ImageField::new('fileDocument', 'Document PDF')->setFormType(FileType::class)
->setBasePath('docs');
return [
IdField::new('id')->onlyOnIndex(),
TextField::new('nomDocument', 'Titre'),
DateTimeField::new('created_at', 'Date de création'),
TextField::new('fileDocument', 'Document PDF')
->hideOnIndex()
->setFormType(FileType::class, [
'constraints' => [
new File([
'maxSize' => '1024k',
'mimeTypes' => [
'application/pdf',
'application/x-pdf',
],
'mimeTypesMessage' => 'Veuillez télécharger un document PDF valide',
])
],
]),
];
}
public function configureActions(Actions $actions): Actions
{
return $actions
->add(Crud::PAGE_INDEX, Action::DETAIL);
}
}
I don't know how I can implement the same configuration in easy admin.
Look Here this is what happens when i create a document from table EasyAdmin.
Thank you.
That's a little weird but you need to use the ImageField (https://symfony.com/bundles/EasyAdminBundle/current/fields/ImageField.html)
And in you CrudController:
public function configureFields(string $pageName): iterable{
return [
ImageField::new('pdf', 'Your PDF')
->setFormType(FileUploadType::class)
->setBasePath('documents/') //see documentation about ImageField to understand the difference beetwen setBasePath and setUploadDir
->setUploadDir('public/documents/')
->setColumns(6)
->hideOnIndex()
->setFormTypeOptions(['attr' => [
'accept' => 'application/pdf'
]
]),
];
}
See documentation about ImageField to understand the difference beetwen setBasePath and setUploadDir
----- EDIT ----
In your index page of CRUD, you can create a link for your file like this:
public function configureFields(string $pageName): iterable{
return [
ImageField::new('pdf', 'Your PDF')
->setFormType(FileUploadType::class)
->setBasePath('documents/') //see documentation about ImageField to understand the difference beetwen setBasePath and setUploadDir
->setUploadDir('public/documents/')
->setColumns(6)
->hideOnIndex()
->setFormTypeOptions(['attr' => [
'accept' => 'application/pdf'
]
]),
TextField::new('pdf')->setTemplatePath('admin/fields/document_link.html.twig')->onlyOnIndex(),
];
}
Your templates/admin/fields/document_link.html.twig :
{% if field.value %}
Download file
{% else %}
--
{% endif %}

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',
]);

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