Silverstripe 3 Extending Upload Field with nice error messages - silverstripe

I've made a custom upload field to extract zip files automatically. I can't figure out how to throw 'nice' error messages that are readable to a end-user.
I want the errors to come from the saveTemporaryFile method.
eg (something like this would be great):
return $this->setError("My custom error");

Error messages are usually thrown in the validate function where you have access to the form field's Validator object. For instance:
<?php
class MyUploadField extends UploadField {
// ...
private $temporaryFileSaveSuccessful = false;
private function saveTemporaryFile() {
// Do extract logic
// set $this->temporaryFileSaveSuccessful appropriately
}
public function validate($validator) {
if(!$this->temporaryFileSaveSuccessful) {
$validator->validationError('ExtractionUnsuccessful',
'Unable to extract your zip file',
'validation'
);
return false;
}
return parent::validate($validator);
}
// ...
}
However this has the one issue that the validate function is called on form submission, so depending on your code saveTemporaryFile may not have been run by that time. You could possibly run the extraction earlier by creating a Form subclass and overriding the httpSubmission function.

Related

How to set validation state in a custom validation handler in a Blazor EditForm

I am struggling with a custom validation in an EditForm using basic validation in my Blazor server-side app. In this MS doc it is stated
A handler for the OnValidationRequested event of the EditContext
executes custom validation logic. The handler's result updates the
ValidationMessageStore instance.
but nothing is mentioned on how to inform the validation logic if the custom validation was successful or failed. The code below shows how error messages can be added in a custom validation handler, but OnValidSubmit is called during the await-ing of the DB query in the custom validation handler. So it seems necessary, to perform the custom validation also in OnValidSubmit, which is acceptable in my case but not nice.
Q: Is there no other/nicer way to inform the validation logic about the result of a custom validation in order to prevent having to re-check inside OnValidSubmit?
Here is my code in OnParametersSetAsync:
// Create EditContext
editContext = new EditContext(assignment);
// Create additional message store for the custom validation messages
validationMessageStore = new(editContext);
// Add additional validation handler
editContext.OnValidationRequested += OnValidationRequestedAsync;
Here is the code for the custom validation handler:
private async void OnValidationRequestedAsync(object sender, ValidationRequestedEventArgs e)
{
// clear previous error messages
validationMessageStore.Clear();
// check DB if Title already exists
bool exists = await myService.IsPresent(myModel.Title);
// While waiting for this async process, OnValidSubmit gets called
if (exists)
{
// yes, so add a validation message
validationMessageStore.Add(() => myModel.Title, "The Title is already used.");
// inform ValidationSummary that a new error message has been added
editContext.NotifyValidationStateChanged();
}
}
In OnValidSubmit I have to re-check:
// check DB if Title already exists
bool exists = await myService.IsPresent(myModel.Title);
if (exists)
{
// do nothing if invalid (error message is displayed by OnValidationRequestedAsync)
return;
}
// ...
Have you looked at Blazored.Validation? It takes all the pain out of this. You create a validator (or you can use attributes, but for custom validation, I find the validator approach much easier), then simply add the <FluentValidationValidator /> tag inside your form, and it all just works. Validation is fired as appropriate, messages are shown as expected and you OnValidSubmit just doesn't get called if there are validation errors.
Hope that helps.
I found a possible solution (at least in general) by decorating the property with a custom validation attribute. Just add
[CustomValidation(typeof(myModelClass), "OnValidateTitle")]
public string Title{ get; set; }
to the property of the model class and add the static method in the model class that performs the custom validation and returns an appropriate status
public static ValidationResult OnValidateTitle(string title)
{
if (...)
{
return new ValidationResult("Title is ...");
}
else
{
return ValidationResult.Success;
}
}
While this approach should work in general, in my particular case it didn't work, because I do not have access to the database context from the static method.

Symfony: Logging all Mails created by Mailer

For various reasons I want/need to log all emails sent through my website which runs on Symfony 5.
What I have so far is a subscriber that creates an Entity of type EmailLogEntry when a MessageEvent class is created (at least that's what I understand from (MessageEvent::class) - correct me if I'm wrong). I also use this subscriber to fill in missing emailadresses with the default system address.
Now, after sending the email, I'd like to adjust my entity and call $email->setSent(true);, but I can't figure out how to subscribe to the event that tries to send the email. And for the reusability of the code I don't want to do that in the Services (yes, it's multiple since there's multiple sources that generate mails) where I actually call $this->mailer->send($email);.
My questions now are:
Can someone tell me how I can subscribe to the Mailers send event?
How, in general, do I figure out what events I can subscribe to? The kernel events are listed in the documentation, but what about all the other events that are fired?
Btw, my subscriber code at the moment:
class SendMailSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
MessageEvent::class => [
['onMessage', 255],
['logMessage', 0],
],
];
}
public function logMessage(MessageEvent $event) {
$email = new EmailLogEntry();
[...]
}
}
Thanks.
The answer to my question is: at the moment you can not subscribe to the send() event of Mailer.
As a workaround, that's my code for now:
It's an extract from my Mailer service.
// send email and log the whole message
private function logSendEmail($email) {
$log = new EmailLog();
// sender
$log->setSender(($email->getFrom()[0]->getName() ?:"")." <".$email->getFrom()[0]->getAddress().">");
// get recipient list
foreach($email->getTo() AS $to) {
$recipient[] = "To: ".$to->getName()." <".$to->getAddress().">";
}
foreach($email->getCc() AS $cc) {
$recipient[] = "CC: ".$cc->getName()." <".$cc->getAddress().">";
}
foreach($email->getBcc() AS $bcc) {
$recipient[] = "Bcc: ".$bcc->getName()." <".$bcc->getAddress().">";
}
$log->setRecipient(implode(";",$recipient));
// other message data
$log->setSubject($email->getSubject());
$log->setMessage(serialize($email->__serialize()));
$log->setMessageTime(new \DateTime("now"));
$log->setSent(0); // set Sent to 0 since mail has not been sent yet
// try to send email
try {
$this->mailer->send($email);
$log->setSent(1); // set sent to 1 if mail was sent successfully
}
// catch(Exception $e) {
// to be determined
// }
// and finally persist entity to database
finally {
$this->em->persist($log);
$this->em->flush();
}
}
EmailLog is an Entity I created. There's a slight overhang as I save the sender, recipients and subject seperatly. However, in compliance with consumer data protection, I plan to clear the message field automatically after 30 days while holding on to the other fields for 6 months.
I'm also not using a subscriber as of now, mostly because website visitors can request a message copy and I'm not interested in logging that as well. Also, since I want to produce reusable code, I might at some point face the problem that mails could contain personal messages and I certainly will not want to log this mails.

Api-Platform: using PUT for creating resources

I would like to use the PUT method for creating resources. They are identified by an UUID, and since it is possible to create UUIDs on the client side, I would like to enable the following behaviour:
on PUT /api/myresource/4dc6efae-1edd-4f46-b2fe-f00c968fd881 if this resource exists, update it
on PUT /api/myresource/4dc6efae-1edd-4f46-b2fe-f00c968fd881 if this resource does not exist, create it
It's possible to achieve this by implementing an ItemDataProviderInterface / RestrictedDataProviderInterface.
However, my resource is actually a subresource, so let's say I want to create a new Book which references an existing Author.
My constructor looks like this:
/**
* Book constructor
*/
public function __construct(Author $author, string $uuid) {
$this->author = $author;
$this->id = $uuid;
}
But I don't know how to access the Author entity (provided in the request body) from my BookItemProvider.
Any ideas?
In API Platform many things that should occur on item creation is based on the kind of request it is. It would be complicated to change.
Here are 2 possibilities to make what you want.
First, you may consider to do a custom route and use your own logic. If you do it you will probably be happy to know that using the option _api_resource_class on your custom route will enable some listeners of APIPlaform and avoid you some work.
The second solution, if you need global behavior for example, is to override API Platform. Your main problem for this is the ReadListener of ApiPlatform that will throw an exception if it can't found your resource. This code may not work but here is the idea of how to override this behavior:
class CustomReadListener
{
private $decoratedListener;
public function __construct($decoratedListener)
{
$this->decoratedListener = $decoratedListener;
}
public function onKernelRequest(GetResponseEvent $event)
{
try {
$this->decoratedListener->onKernelRequest($event);
} catch (NotFoundHttpException $e) {
// Don't forget to throw the exception if the http method isn't PUT
// else you're gonna break the 404 errors
$request = $event->getRequest();
if (Request::METHOD_PUT !== $request->getMethod()) {
throw $e;
}
// 2 solutions here:
// 1st is doing nothing except add the id inside request data
// so the deserializer listener will be able to build your object
// 2nd is to build the object, here is a possible implementation
// The resource class is stored in this property
$resourceClass = $request->attributes->get('_api_resource_class');
// You may want to use a factory? Do your magic.
$request->attributes->set('data', new $resourceClass());
}
}
}
And you need to specify a configuration to declare your class as service decorator:
services:
CustomReadListener:
decorate: api_platform.listener.request.read
arguments:
- "#CustomReadListener.inner"
Hope it helps. :)
More information:
Information about event dispatcher and kernel events: http://symfony.com/doc/current/components/event_dispatcher.html
ApiPlatform custom operation: https://api-platform.com/docs/core/operations#creating-custom-operations-and-controllers
Symfony service decoration: https://symfony.com/doc/current/service_container/service_decoration.html

Where to place validation code

I've created a simple form with an enum field on a grid, dragged from the DataSource CompanyImage:
Table CompanyImage has an Index on this field named Brand in my example and AllowDuplicates is set to No :
And here is the form:
I've overridden the close() method of the form like this:
public void close()
{
CompanyImage_ds.write();
super();
}
An error is displayed when I close it saying that
"Cannot create a record in CompanyImage(CompanyImage). Legal entities: Example1.
The record already exists."
That's fine but I would like a way to stop closing the window when this happens. A validateWrite() would be nice but I am not really able to figure out where and what to write in order to accomplish this behavior.
I mean, how to check that new row is added and it contains a field that already exists in the table ?
You shouldn't have to force the write() method. Closing the form should already do it.
If you wish to check something to allow the form to be closed, the close() method is too late in execution. You should leverage the canClose() method.
You could override the validate method of the grid column. You would need to write some validation logic in that method but that would prevent the column from saving at all if validation failed.
public boolean validate()
{
boolean ret;
// write your own validation logic
if (validation logic is true)
{
ret = true;
}
return ret;
}

passing information from a Drupal Forms API validation function to a submit function

In my validation function for a Drupal Forms API form, I attempt to charge the user's credit card. If it succeeds, I'd like to pass the reference number to the submit function so it can be used there. What's the best way of doing this?
The documentation says this:
Note that as of Drupal 6, you can also simply store arbitrary variables in $form['#foo'] instead, as long as '#foo' does not conflict with any other internal property of the Form API.
So you could do something like this:
function form($form_state) {
//in your form definition function:
$form['#reference_number'] = 0;
}
function form_validate($form, &$form_state) {
//try to charge card ...
if ($card_charged) {
$form_state['values']['#reference_number'] = $reference_number;
}
}
function submit_form($form, &$form_state) {
if (isset($form_state['values']['#reference_number'])) {
$reference_number = $form_state['values']['#reference_number'];
//do whatever you want
}
}

Resources