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.
Related
I build a FormType in Symfony5 and make use of DataTransformer on 2 fields :
compagnie_princ
compagnie_sec.
DataTransformer basically takes an object ID and renders It to a label.
DataTransformer works fine, when Form is initially rendered on browser, see below capture:
Problem is after validation callbacks are executed, If an error occured, It fails to transform my Id back to a text value.
Code samples (most important parts) :
AccordCommercialFormType.php
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('compagnie_princ', TextType::class,
[
'label' => 'forms.parameter.accord.compagnie_princ',
]
)
->add('compagnie_sec', TextType::class,
[
'label' => 'forms.parameter.accord.compagnie_sec',
]
);
/** ... **/
$builder>addEventListener(
FormEvents::PRE_SET_DATA,
[$this, 'onPreSetData']
)
->addEventListener(
FormEvents::PRE_SUBMIT,
[$this, 'onPreSubmit']
);
$builder->get('compagnie_princ')->addModelTransformer($this->transformer);
$builder->get('compagnie_sec')->addModelTransformer($this->transformer);
}
Events are captured on preSubmit to fetch ID, because fields 'compagnie_princ' and 'compagnie_sec' are autocompleted with AJAX, populating hidden inputs. My guess is something is going wrong on that part.
public function onPreSetData(FormEvent $event): void
{
$form = $event->getForm();
$form->add('compagnie_princ_id', HiddenType::class,
['mapped' => false,
'attr' => ['class' => 'hidden-field'],
'data' => $event->getData()->getCompagniePrinc() ? $event->getData()->getCompagniePrinc()->getId() : null
]
);
$form->add('compagnie_sec_id', HiddenType::class,
['mapped' => false,
'attr' => ['class' => 'hidden-field'],
'data' => $event->getData()->getCompagnieSec() ? $event->getData()->getCompagnieSec()->getId() : null,
]
);
}
public function onPreSubmit(FormEvent $event): void
{
$data = $event->getData();
$data['compagnie_princ'] = (int)$data['compagnie_princ_id'];
$data['compagnie_sec'] = (int)$data['compagnie_sec_id'];
$event->setData($data);
}
CompagnieToIdTransformer.php
class CompagnieToIdTransformer implements DataTransformerInterface
{
public function __construct(private EntityManagerInterface $em){
}
public function transform($compagnie)
{
if (null === $compagnie) {
return '';
}
return $compagnie->getCodeIata();
}
public function reverseTransform($compagnieId):?Compagnie
{
if (!$compagnieId) {
return null;
}
$compagnie = $this->em
->getRepository(Compagnie::class)
->find($compagnieId)
;
if (null === $compagnie) {
throw new TransformationFailedException(sprintf(
'A company with number "%s" does not exist!',
$compagnieId
));
}
return $compagnie;
}
}
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 %}
Is there way to filter collection, e.g.
class Company{
/**
* #ORM\OneToMany(targetEntity="App\Entity\User", mappedBy="company", cascade={"persist","remove"})
*/
public $users;
}
on way to check that company have users. But i need to filter on company side, so send request /api/companies?somefilter.
So point is there way to check is collection empty?
You can add a boolean column in companies where you set to true when create a user relation.
So you can add a BooleanFilter to you company entity for check companies which have users.
/**
* #ApiResource
* #ApiFilter(BooleanFilter::class, properties={"hasUsers"})
*/
Or you can create a CustomFilter where you input true o false and get companies with users throught queryBuilder
https://api-platform.com/docs/core/filters/#creating-custom-doctrine-orm-filters
<?php
// api/src/Filter/RegexpFilter.php
namespace App\Filter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Doctrine\ORM\QueryBuilder;
final class RegexpFilter extends AbstractContextAwareFilter
{
protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
{
// otherwise filter is applied to order and page as well
if (
!$this->isPropertyEnabled($property, $resourceClass) ||
!$this->isPropertyMapped($property, $resourceClass)
) {
return;
}
$parameterName = $queryNameGenerator->generateParameterName($property);
// Generate a unique parameter name to avoid collisions with other filters
$queryBuilder
->andWhere(sprintf('REGEXP(o.%s, :%s) = 1', $property, $parameterName))
->setParameter($parameterName, $value);
}
// This function is only used to hook in documentation generators (supported by Swagger and Hydra)
public function getDescription(string $resourceClass): array
{
if (!$this->properties) {
return [];
}
$description = [];
foreach ($this->properties as $property => $strategy) {
$description["regexp_$property"] = [
'property' => $property,
'type' => 'string',
'required' => false,
'swagger' => [
'description' => 'Filter using a regex. This will appear in the Swagger documentation!',
'name' => 'Custom name to use in the Swagger documentation',
'type' => 'Will appear below the name in the Swagger documentation',
],
];
}
return $description;
}
}
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.
I have a REST API and have an Entity Userwith field called Avatar, in DB I save name XXXX.jpg but when I return I want to add a url in this field Avatar, for example www.mylink.com/XXXX.jpg.
I'm trying with a service implements SubscribingHandlerInterfacebut I don't know how I can use it.
I have this method in this service:
class UrlManager implements SubscribingHandlerInterface
{
public static function getSubscribingMethods()
{
return array(
array(
'direction' => GraphNavigator::DIRECTION_SERIALIZATION,
'format' => 'json',
'type' => 'AppBundle/Entity/User',
'method' => 'serializeUrlAvatar',
),
);
}
public function serializeUrlAvatar(User $user)
{
$url = 'www.mylink.com';
return array(
"avatar" => $url . $user->getAvatar()
);
}
}
but how can I call this service to modify url when I serialize.
Now I do this:
$_format = 'json';
$json = $this->get('jms_serializer')->serialize($user, $_format);
return new Response($json, 200, ['Content-Type' => 'application/' . $_format]);
In service.yml:
app.url_converter_service:
class: AppBundle\Service\UrlManager
tags:
- { name: jms_serializer.subscribing_handler }
Update
In my controller I call this function like this:
$result = $this->get('app.url_converter_service')->serializeUrlAvatar($user);
$json = $this->get('jms_serializer')->serialize($result, $_format);
return new Response($json, 200, ['Content-Type' => 'application/' . $_format]);
So my question is, exists a way to remove the first line and serialize correctly (add the url) when I serialize?
Have you registered your service like this?
# app/config/services.yml
avatar_url_handler:
class: YourBundle\Serializer\Handler\AvatarUrlHandler
tags:
- { name: jms_serializer.subscribing_handler }
I found a solution. I create a service which implements EventSubscriberInterface like this:
class UserSerializeHandler implements EventSubscriberInterface
{
private $user_uploads;
public function __construct($user_uploads){
$this->user_uploads = $user_uploads;
}
public static function getSubscribedEvents()
{
return array(
array(
'event' => 'serializer.pre_serialize',
'class' => User::class,
'method' => 'onPreSerializeUser'
));
}
public function onPreSerializeUser(PreSerializeEvent $event)
{
/** #var User $user */
$user = $event->getObject();
$avatar = $user->getAvatar();
$user->setAvatar($this->user_uploads . "/" . $avatar);
}
}
In service.yml:
app.serializer_user_service:
class: AppBundle\Service\UserSerializeHandler
arguments: ['%user_uploads%']
tags:
- { name: jms_serializer.event_subscriber }
I have user_uploads in parameters.yml like this:
user_uploads: 'https://myUrl.com'
And in any Controller that I serialize a User, I add the url in the Avatar paramter.
$json = $this->get('jms_serializer')->serialize($user, $_format);
return new Response($json, 200, ['Content-Type' => 'application/' . $_format]);