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;
}
}
Related
I have this form: https://greektoenglish.com/translation
After I complete the form, provide it with a file, and finally submit it, I get this error: "field is required". That the file field is required. But I already completed the field.
If I remove "'#required' => TRUE," from the code where the file upload field is declared, fill the form out, and submit it, then the form is submitted correctly.
How can I solve this?
This is my code:
<?php
namespace Drupal\submit_translation\Form;
use Drupal\Component\Utility\EmailValidatorInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Mail\MailManagerInterface;
use Drupal\mimemail\Utility\MimeMailFormatHelper;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* The example email contact form.
*/
class SubmitTranslation extends FormBase {
/**
* The email.validator service.
*
* #var \Drupal\Component\Utility\EmailValidatorInterface
*/
protected $emailValidator;
/**
* The language manager service.
*
* #var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* The mail manager service.
*
* #var \Drupal\Core\Mail\MailManagerInterface
*/
protected $mailManager;
/**
* Constructs a new ExampleForm.
*
* #param \Drupal\Component\Utility\EmailValidatorInterface $email_validator
* The email validator service.
* #param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager service.
* #param \Drupal\Core\Mail\MailManagerInterface $mail_manager
* The mail manager service.
*/
public function __construct(EmailValidatorInterface $email_validator, LanguageManagerInterface $language_manager, MailManagerInterface $mail_manager) {
$this->emailValidator = $email_validator;
$this->languageManager = $language_manager;
$this->mailManager = $mail_manager;
}
/**
* {#inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('email.validator'),
$container->get('language_manager'),
$container->get('plugin.manager.mail')
);
}
/**
* {#inheritdoc}
*/
public function getFormId() {
return 'submit_translation_form';
}
/**
* {#inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $dir = NULL, $img = NULL) {
$form['intro'] = [
'#markup' => $this->t('Use this form to send us the document that we\'ll translate!'),
];
$form['from'] = [
'#type' => 'textfield',
'#title' => $this->t('Name'),
'#description' => $this->t("Your full name."),
'#required' => TRUE,
];
$form['from_mail'] = [
'#type' => 'textfield',
'#title' => $this->t('Email address'),
'#description' => $this->t("Your email address."),
'#required' => TRUE,
];
$form['params'] = [
'#tree' => TRUE,
'subject' => [
'#type' => 'textfield',
'#title' => $this->t('Title'),
'#description' => $this->t("The title of the document."),
'#required' => TRUE,
],
'count' => [
'#type' => 'textfield',
'#title' => $this->t('Word Count'),
'#description' => $this->t("The word count of the document."),
'#required' => TRUE,
],
'body' => [
'#type' => 'textarea',
'#title' => $this->t('Comments'),
'#description' => $this->t("Tell us if you have any special requirements."),
'#required' => TRUE,
],
// This form element forces plaintext-only email when there is no HTML
// content (that is, when the 'body' form element is empty).
'plain' => [
'#type' => 'hidden',
'#states' => [
'value' => [
':input[name="body"]' => ['value' => ''],
],
],
],
'attachments' => [
'#name' => 'files[attachment]',
'#type' => 'file',
'#title' => $this->t('Choose a file to send for translation.'),
'#required' => TRUE,
],
];
$form['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Send message'),
];
return $form;
}
/**
* {#inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
// Extract the address part of the entered email before trying to validate.
// The email.validator service does not work on RFC2822 formatted addresses
// so we need to extract the RFC822 part out first. This is not as good as
// actually validating the full RFC2822 address, but it is better than
// either just validating RFC822 or not validating at all.
$pattern = '/<(.*?)>/';
$address = $form_state->getValue('from_mail');
preg_match_all($pattern, $address, $matches);
$address = isset($matches[1][0]) ? $matches[1][0] : $address;
if (!$this->emailValidator->isValid($address)) {
$form_state->setErrorByName('from_mail', $this->t('That email address is not valid.'));
}
$file = file_save_upload('attachment', [ 'file_validate_extensions' => array('doc docx pdf')], 'temporary://', 0);
if ($file) {
$form_state->setValue(['params', 'attachments'], [['filepath' => $file->getFileUri()]]);
}
}
/**
* {#inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// First, assemble arguments for MailManager::mail().
$module = 'submit_translation';
$key = "solon_key";
$to = "info#gexl.eu";
$langcode = $this->languageManager->getDefaultLanguage()->getId();
$params = $form_state->getValue('params');
$reply = "";
$send = TRUE;
$params['body'] .= " Count: " . $params['count'];
// Second, add values to $params and/or modify submitted values.
// Set From header.
if (!empty($form_state->getValue('from_mail'))) {
$params['headers']['From'] = MimeMailFormatHelper::mimeMailAddress([
'name' => $form_state->getValue('from'),
'mail' => $form_state->getValue('from_mail')
]);
}
elseif (!empty($form_state->getValue('from'))) {
$params['headers']['From'] = $from = $form_state->getValue('from');
}
else {
// Empty 'from' will result in the default site email being used.
}
// Handle empty attachments - we require this to be an array.
if (empty($params['attachments'])) {
$params['attachments'] = [];
}
// Remove empty values from $param['headers'] - this will force the
// the formatting mailsystem and the sending mailsystem to use the
// default values for these elements.
foreach ($params['headers'] as $header => $value) {
if (empty($value)) {
unset($params['headers'][$header]);
}
}
// Finally, call MailManager::mail() to send the mail.
$result = $this->mailManager->mail($module, $key, $to, $langcode, $params, $reply, $send);
if ($result['result'] == TRUE) {
$this->messenger()->addMessage($this->t('Your message has been sent.'));
}
else {
// This condition is also logged to the 'mail' logger channel by the
// default PhpMail mailsystem.
$this->messenger()->addError($this->t('There was a problem sending your message and it was not sent.'));
}
}
}
This happens because the form element '#type' => 'file' has no #value to validate. #required fields must have a #value set otherwise validation fails.
This is (now considered) a very old issue that has been fixed in Drupal 9.5.x, but this was assumed in the good old days of Drupal 7, as mentioned in the Form API reference :
#required: Indicates whether or not the element is required. This
automatically validates for empty fields, and flags inputs as
required. File fields are NOT allowed to be required.
So I guess the best solution is to upgrade to 9.5.x or above, if feasible, but as sometimes upgrading makes things complicated, you might prefer to review and apply the patch manually to your current code base.
[EDIT]: If still having issues after upgrade to >= 9.5.2,
Looking at the patch, a default valueCallback is now used to provide a #value to file form elements, but.. well there is another issue :
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
if ($input === FALSE) {
return NULL;
}
$parents = $element['#parents'];
$element_name = array_shift($parents); # <- problem here :/
$uploaded_files = \Drupal::request()->files->get('files', []);
$uploaded_file = $uploaded_files[$element_name] ?? NULL;
if ($uploaded_file) {
// Cast this to an array so that the structure is consistent regardless of
// whether #value is set or not.
return (array) $uploaded_file;
}
return NULL;
}
See how it doesn't care about whether or not the element has a #name explicitly defined ? and whether or not #parents is a tree ? Now because of those wrong assumptions on the element's name and its parents, you are somehow forced to either :
Leave the #name property unset and refer to the file later on validation/submit as 'params' (the parents root) instead of 'attachment'. Or,
Stick with #tree => FALSE. Or,
Provide your own #value_callback (deprecated ...?)
I simply want an API endpoint that matches two fields, registrationId and hash.
I cannot find any examples of this online, I can only single matches, regex matching, OR clauses etc. Nowhere is there an example of just just querying the database to return a row that matches two columns.
I have a table called registrationhashes. In the entity I have added this line:
#[ApiFilter(RegistrationSearchFilter::class, properties: ['registrationId', 'hash'])]
And then I have gotten so far with writing a custom filter:
final class RegistrationSearchFilter extends AbstractFilter
{
protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, Operation $operation = null, array $context = []): void
{
if ($property !== 'search') {
return;
}
$alias = $queryBuilder->getRootAliases()[0];
$queryBuilder->andWhere(sprintf('%s.registrationId = :search AND %s.hash = :hash', $alias, $alias));
}
public function getDescription(string $resourceClass): array
{
if (!$this->properties) {
return [];
}
$description = [];
foreach ($this->properties as $property => $strategy) {
$description["regexp_$property"] = [
'property' => $property,
'type' => Type::BUILTIN_TYPE_STRING,
'required' => false,
'description' => 'Filter using a regex. This will appear in the OpenApi documentation!',
'openapi' => [
'example' => 'Custom example that will be in the documentation and be the default value of the sandbox',
'allowReserved' => false,// if true, query parameters will be not percent-encoded
'allowEmptyValue' => true,
'explode' => false, // to be true, the type must be Type::BUILTIN_TYPE_ARRAY, ?product=blue,green will be ?product=blue&product=green
],
];
}
return $description;
}
}
I have just put the description function in there copied from somewhere else for now.
But as you can see from the WHERE clause I ned to match registrationId AND hash, but I only have one value to match with.
How can I achieve this?
Look at the search filter docs, you will see an example of combining two search filters. I believe this accomplishes your task without the need for a custom filter.
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
#[ApiResource]
#[ApiFilter(SearchFilter::class, properties: ['registrationId' => 'exact', 'hash' => 'exact'])]
class RegistrationHash
{
// ...
}
The endpoint would then be something like:
http://localhost:8000/api/registrationhashes?registrationId=10&hash=3C76B43F
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.
Having a ManyToMany relation between Article and Tag i have a form with a multiselect of type EntityType using Symfony FormBuilder.
Everything seems fine, even preselection, except when deselecting an already related Tag from the tags-select, it does not remove the underlying relation. Therefor I can only add more relations.
I tried removing all relations before saving but because of LazyLoading I get other trouble there. I cannot find anything regarding this in the documentation but this might be very common basics, right?
Can anyone help me out here?
This is the code of my EntityType-Field
$builder->add('tags', FormEntityType::class, [
'label' => 'Tags',
'required' => false,
'multiple' => true,
'expanded' => false,
'class' => Tag::class,
'choice_label' => function (Tag $tag) {
return $tag->getName();
},
'query_builder' => function (TagRepository $er) {
return $er->createQueryBuilder('t')
->where('t.isActive = true')
->orderBy('t.sort', 'ASC')
->addOrderBy('t.name', 'ASC');
},
'choice_value' => function(?Tag $entity) {
return $tag ? $tag->getId() : '';
},
'choice_attr' => function ($choice) use ($options) {
// Pre-Selection
if ($options['data']->getTags() instanceof Collection
&& $choice instanceof Tag) {
if ($options['data']->getTags()->contains($choice)) {
return ['selected' => 'selected'];
}
}
return [];
}
]);
And this is how I save after form-submit
$articleForm = $this->createForm(ArticleEditFormType::class, $article)->handleRequest($this->getRequest());
if ($articleForm->isSubmitted() && $articleForm->isValid()) {
// saving the article
$article = $articleForm->getData();
$this->em->persist($article);
$this->em->flush();
}
The Relations look like this:
Article.php
/**
* #ORM\ManyToMany(targetEntity=Tag::class, mappedBy="articles", cascade={"remove"})
* #ORM\OrderBy({"sort" = "ASC"})
*/
private $tags;
public function removeTag(Tag $tag): self
{
if ($this->tags->removeElement($tag)) {
$tag->removeArticle($this);
}
return $this;
}
Tag.php
/**
* #ORM\ManyToMany(targetEntity=Article::class, inversedBy="tags", cascade={"remove"})
* #ORM\OrderBy({"isActive" = "DESC","name" = "ASC"})
*/
private $articles;
public function removeArticle(Article $article): self
{
$this->articles->removeElement($article);
return $this;
}
Setters, adders, etc. won't be called on the entity by default.
In the case of a multiselect where the underlying object is a collection, to ensure that the element is removed by calling removeArticle you have to set the option by_reference to false.
This is applicable to CollectionType as well.
Problem description
I need to define object-related field (like sonata_type_model_list) inside sonata_type_immutable_array form type definition:
$formMapper->add('options', 'sonata_type_immutable_array', array(
'keys' => array(
array('linkName', 'text', array()),
array('linkPath', 'sonata_type_model_list',
array(
'model_manager' => $linkAdmin->getModelManager(),
'class' => $linkAdmin->getClass(),
)
)
)
)
This is not working, here is error message:
Impossible to access an attribute ("associationadmin") on a NULL variable ("") in SonataDoctrineORMAdminBundle:Form:form_admin_fields.html.twig at line 60
What I found about this problem
I tryed to find any information about using sonata_type_model_list inside sonata_type_immutable_array, but there is very little information.
This (https://github.com/a2lix/TranslationFormBundle/issues/155) topic helped me a bit, but doing all in the same manner I've got another error:
Impossible to invoke a method ("id") on a NULL variable ("") in SonataDoctrineORMAdminBundle:Form:form_admin_fields.html.twig at line 60
So I totally failed in uderstanding what I have to do.
My context
-- I have Doctrine ORM Mapped class called CmsLink, it defines object to which 'linkPath' field relates.
-- I have admin class for CmsLink class, it has very basic configuration:
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('technicalAlias')
->add('url')
;
}
-- I have Doctrine ORM Mapped class called CmsMenuItem, it defines object and 'options' filed which persists data managed by sonata_type_immutable_array form type, field type is json_array:
/**
* #var string
*
* #ORM\Column(name="options", type="json_array", nullable=true)
*/
private $options;
-- And finally I have admin class for CmsMenuItem class, here is the key code piece:
$linkAdmin = $this->configurationPool->getAdminByClass("Argon\\CMSBundle\\Entity\\CmsLink");
$formMapper
->add('options', 'sonata_type_immutable_array',
array(
'keys' => array(
array('linkName', 'text', array()),
array('linkPath', 'sonata_type_model_list',
array(
'model_manager' => $linkAdmin->getModelManager(),
'class' => $linkAdmin->getClass(),
)
),
array('description', 'textarea', array()),
array('image', 'sonata_media_type',
array(
'provider' => 'sonata.media.provider.image',
'context' => 'pages_static',
'required'=>false,
)
)
)
)
);
Question goals
Find out what I need to do to bring life into this idea?
Get general information and understanding abot how to include object-related field types into sonata_type_immutable_array
I've just come across this problem, and solved it with a custom type and data transformers.
Here's the rough outline, though you need to tailor it to your problem.
Custom Type
class YourImmutableArrayType extends ImmutableArrayType
{
/**
* #var YourSettingsObjectTransformer
*/
private $transformer;
public function __construct(YourSettingsObjectTransformer $transformer)
{
$this->transformer = $transformer;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder->addModelTransformer($this->transformer);
}
public function getName()
{
return 'your_type_name';
}
public function getParent()
{
return 'sonata_type_immutable_array';
}
}
Custom model transformer
class NewsListingSettingsTransformer implements DataTransformerInterface
{
public function __construct(ObjectManager $manager)
{
// You'll need the $manager to lookup your objects later
}
public function reverseTransform($value)
{
if (is_null($value)) {
return $value;
}
// Here convert your objects in array to IDs
return $value;
}
public function transform($value)
{
if (is_null($value)) {
return $value;
}
// Here convert ids embedded in your array to objects,
// or ArrayCollection containing objects
return $value;
}
}
Building form in admin class
$formMapper->add('settings', 'your_type_name', array(
'keys' => array(
array(
'collectionOfObjects',
'sonata_type_model',
array(
'class' => YourObject::class,
'multiple' => true,
'model_manager' => $this->yourObjectAdmin->getModelManager()
)
)
)
));
}
Again, it's a rough outline, so tweak it to your needs.