Is there a way to access the context of a whole form inside a validator of a fieldset-element? It seems the standard behaviour is that the context contains just the form-data of the fieldset where the element resides.
This is my custom validator:
/**
* Returns if the given value is valid
*
* #param string $value
* #param array $context
* #return boolean
*/
public function isValid($value, array $context = array())
{
$this->setValue($value);
$bookingId = isset($context['auftrag_buchung_id'])
? (int) $context['auftrag_buchung_id']
: null;
try {
$anlieferung = new \DateTime($value);
} catch (\Exception $e) {
$this->error(self::NOT_DATETIME);
return false;
}
try {
$booking = $this->table->getById($this->user, $bookingId);
} catch (\Exception $e) {
$this->error(self::NOT_FOUND);
return false;
}
if (!$this->isInBookingRange($value, $booking)) {
$this->error(self::NOT_IN_BOOKINGRANGE);
return false;
}
return true;
}
My problem is $context has just the form-data of the fieldset where i configured this validator. But I have to check against a value that is not inside the fieldset
It should be possible to inject the form instance into the validator when assigning the validator to the form using an InputFilter.
//
$form = new \Zend\Form\Form();
/* ..following could be within __construct() Method of the Form object */
//
$element = new \Zend\Form\Element\Text('text');
$form->add($element);
//
$inputFilter = new \Zend\InputFilter\InputFilter();
//
$textInput = new \Zend\InputFilter\Input('text');
// Add validator with injected form instance
$textInput->getValidatorChain()
->attach(new CustomValidator($form));
$inputFilter->add($textInput)
//
$form->setInputFilter($inputFilter);
With thi ssettings you should be able to use the form data within your validation class:
class CustomValidator {
private $form;
public function __construct($form) {
$this->form = $form;
}
public function isValid($value) {
// Access form data
$this->form->getData();
}
}
Related
I am new with laravel so please don't be harsh.
I am bulding a simple web which connects to an external API(several endpoints) via Guzzle,fetching some data,cleaning them and storing them.
At the moment -and from one endpoint- i have something the following Job:
public function handle(Client $client)
{
try {
$request= $client->request('GET', 'https://api.url./something', [
'headers' => [
'X-RapidAPI-Key'=> env("FOOTBALL_API_KEY"),
'Accept' => 'application/json'
]
]);
$request = json_decode($request->getBody()->getContents(), true);
foreach ($request as $array=>$val) {
foreach ($val['leagues'] as $id) {
League::firstOrCreate(collect($id)->except(['coverage'])->toArray());
}
}
} catch (GuzzleException $e) {
};
}
Therefore i would like some code recommendations, how can i make my code better from design point of view.
My thoughts are:
a)Bind Guzzle as service provider.
b)use a design pattern for implementing calls to endpoints.URI builder maybe?
Any assistance will be appreciated.
May the force be with you.
Detailed feedback
Some pointers specific to the provided code itself:
A guzzle client request returns a response, which does not match the name of the parameter you assign it to
Calls to json_decode can fail in which case they'll return null. In terms of defensive programming it's good to check for those fail cases
Your case makes some assumptions about the data in the response. It's best to check if the response is in the actual format you expect before using it.
You catch all GuzzleExceptions, but do nothing in those cases. I think you could improve this by either:
Logging the exception
Throwing another exception which you will catch at a class, calling the handle() method
Both of the options above
You could choose to inject the api key, rather than fetching it directly via the env() method. This will prevent issues described in the warning block here
General feedback
It feels like your code is mixing responsibilities, which is considered bad practice. The handle() method now does the following:
Send API requests
Decode API requests
Validate API responses
Parse API responses
Create models
You could consider moving some or all of these to separate classes, like so:
ApiClient which is responsible for sending out requests
ResponseDecoder which is responsible for turning a response into \stdClass
ResponseValidator which is responsible for checking if the response has the expected data structure
RepsonseParser which is responsible for turning the response \stdClass into collections
LeagueFactory which is responsible for turning collections into League models
One could argue that the first four classes should be put into a single class called ApiClient. That's purely up to you.
So in the end you would come up with something like this:
<?php
namespace App\Example;
use Psr\Log\LoggerInterface;
class LeagueApiHandler
{
/**
* #var ApiClient
*/
private $apiClient;
/**
* #var ResponseDecoder
*/
private $decoder;
/**
* #var ResponseValidator
*/
private $validator;
/**
* #var ResponseParser
*/
private $parser;
/**
* #var LeagueFactory
*/
private $factory;
/**
* #var LoggerInterface
*/
private $logger;
public function __construct(
ApiClient $apiClient,
ResponseDecoder $decoder,
ResponseValidator $validator,
ResponseParser $parser,
LeagueFactory $factory,
LoggerInterface $logger
) {
$this->apiClient = $apiClient;
$this->decoder = $decoder;
$this->validator = $validator;
$this->parser = $parser;
$this->factory = $factory;
$this->logger = $logger;
}
public function handle()
{
try {
$response = $this->apiClient->send();
} catch (\RuntimeException $e) {
$this->logger->error('Unable to send api request', $e->getMessage());
return;
};
try {
$decodedResponse = $this->decoder->decode($response);
} catch (\RuntimeException $e) {
$this->logger->error('Unable to decode api response');
return;
};
if (!$this->validator->isValid($decodedResponse)) {
$this->logger->error('Unable to decode api response');
return;
}
$collections = $this->parser->toCollection($decodedResponse);
foreach ($collections as $collection) {
$this->factory->create($collection);
}
}
}
namespace App\Example;
use GuzzleHttp\Client;
class ApiClient
{
/**
* #var Client
*/
private $client;
/**
* #var string
*/
private $apiKey;
public function __construct(Client $client, string $apiKey)
{
$this->client = $client;
$this->apiKey = $apiKey;
}
public function send()
{
try {
return $this->client->request('GET', 'https://api.url./something', [
'headers' => [
'X-RapidAPI-Key' => $this->apiKey,
'Accept' => 'application/json'
]
]);
} catch (GuzzleException $e) {
throw new \RuntimeException('Unable to send request to api', 0, $e);
};
}
}
namespace App\Example;
use Psr\Http\Message\ResponseInterface;
class ResponseDecoder
{
public function decode(ResponseInterface $response): \stdClass
{
$response = json_decode($response->getBody()->getContents(), true);
if ($response === null) {
throw new \RuntimeException('Unable to decode api response');
}
return $response;
}
}
namespace App\Example;
class ResponseValidator
{
public function isValid(\stdClass $response): bool
{
if (is_array($response) === false) {
return false;
}
foreach ($response as $array) {
if (!isset($array['leagues'])) {
return false;
}
}
return true;
}
}
namespace App\Example;
use Illuminate\Support\Collection;
class ResponseParser
{
/**
* #param \stdClass $response
* #return Collection[]
*/
public function toCollection(\stdClass $response): array
{
$collections = [];
foreach ($response as $array => $val) {
foreach ($val['leagues'] as $id) {
$collections[] = collect($id)->except(['coverage'])->toArray();
}
}
return $collections;
}
}
namespace App\Example;
use Illuminate\Support\Collection;
class LeagueFactory
{
public function create(Collection $collection): void
{
League::firstOrCreate($collection);
}
}
Does anybody know how to add extra data on a collection?
The doc says much about how to add extra data on an item which translates into decorating the ItemNormalizer service, and it works pretty well.
But I’m struggling in finding out which normalizer to decorate when it comes to add some data on a collection of entities. The extra data could be anything: the current user logged in, a detailed pager, some debug parameters, ... that are not related to a specific entity, but rather on the request itself.
The only working solution for now is to hook on a Kernel event but that's definitely not the code I like to write:
use ApiPlatform\Core\EventListener\EventPriorities;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
final class SerializeListener implements EventSubscriberInterface
{
/**
* #var Security
*/
private $security;
/**
* #var NormalizerInterface
*/
private $normalizer;
public function __construct(
Security $security,
NormalizerInterface $normalizer
) {
$this->security = $security;
$this->normalizer = $normalizer;
}
public function addCurrentUser(GetResponseForControllerResultEvent $event)
{
$request = $event->getRequest();
if ($request->attributes->has('_api_respond')) {
$serialized = $event->getControllerResult();
$data = json_decode($serialized, true);
$data['hydra:user'] = $this->normalizer->normalize(
$this->security->getUser(),
$request->attributes->get('_format'),
$request->attributes->get('_api_normalization_context')
);
$event->setControllerResult(json_encode($data));
}
}
/**
* #inheritDoc
*/
public static function getSubscribedEvents()
{
return [
KernelEvents::VIEW => [
'addCurrentUser',
EventPriorities::POST_SERIALIZE,
],
];
}
}
Any ideas?
Thank you,
Ben
Alright, I finally managed to do this.
namespace App\Api;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
final class ApiCollectionNormalizer implements NormalizerInterface, NormalizerAwareInterface
{
/**
* #var NormalizerInterface|NormalizerAwareInterface
*/
private $decorated;
public function __construct(NormalizerInterface $decorated)
{
if (!$decorated instanceof NormalizerAwareInterface) {
throw new \InvalidArgumentException(
sprintf('The decorated normalizer must implement the %s.', NormalizerAwareInterface::class)
);
}
$this->decorated = $decorated;
}
/**
* #inheritdoc
*/
public function normalize($object, $format = null, array $context = [])
{
$data = $this->decorated->normalize($object, $format, $context);
if ('collection' === $context['operation_type'] && 'get' === $context['collection_operation_name']) {
$data['hydra:meta'] = ['foo' => 'bar'];
}
return $data;
}
/**
* #inheritdoc
*/
public function supportsNormalization($data, $format = null)
{
return $this->decorated->supportsNormalization($data, $format);
}
/**
* #inheritdoc
*/
public function setNormalizer(NormalizerInterface $normalizer)
{
$this->decorated->setNormalizer($normalizer);
}
}
# config/services.yaml
services:
App\Api\ApiCollectionNormalizer:
decorates: 'api_platform.hydra.normalizer.collection'
arguments: [ '#App\Api\ApiCollectionNormalizer.inner' ]
Keep it for the records :)
Hi need some help with uniting testing a Symfony 2.8 callback.
I don't think I have set it up correctly as the test is passing when I know it should be failing
The entity setup:
The validate callback in the Contact entity:
/**
* Validation callback for contact
* #param \AppBundle\Entity\Contact $object
* #param ExecutionContextInterface $context
*/
public static function validate(Contact $object, ExecutionContextInterface $context)
{
/**
* Check if the country code is valid
*/
if ($object->isValidCountryCode() === false) {
$context->buildViolation('Cannot register in that country')
->atPath('country')
->addViolation();
}
}
The method isValidCountryCode in Contact entity:
/**
* Get a list of invalid country codes
* #return array Collection of invalid country codes
*/
public function getInvalidCountryCodes()
{
return array('IS');
}
The method that checks if the country code is invalid:
/**
* Check if the country code is valid
* #return boolean
*/
public function isValidCountryCode()
{
$invalidCountryCodes = $this->getInvalidCountryCodes();
if (in_array($this->getCountry()->getCode(), $invalidCountryCodes)) {
return false;
}
return true;
}
The validation.yml
AppBundle\Entity\Contact:
properties:
//...
country:
//..
- Callback:
callback: [ AppBundle\Entity\Contact, validate ]
groups: [ "AppBundle" ]
The test class:
//..
use Symfony\Component\Validator\Validation;
class CountryTest extends WebTestCase
{
//...
public function testValidate()
{
$country = new Country();
$country->setCode('IS');
$contact = new Contact();
$contact->setCountry($country);
$validator = Validation::createValidatorBuilder()->getValidator();
$errors = $validator->validate($contact);
$this->assertEquals(1, count($errors));
}
This test returns $errors with a count of 0 but it should be 1 as the country code 'IS' is invalid.
First problem is about the definition of the constraint in the yml files: you need to put the callback under the constraint section instead of properties, so change the validation.yml files as follow:
validation.yml
AppBundle\Entity\Contact:
constraints:
- Callback:
callback: [ AppBundle\Entity\Contact, validate ]
groups: [ "AppBundle" ]
Second in the testCase: you need to take the validator service from the container instead of create a new one with the builder: this object is not initializated with the object structure ect.
Third The callback constraints is defined for the AppBundle validation group only, so pass the validation group to the validator service (As third argument of the service).
So change the testClass as follow:
public function testValidate()
{
$country = new Country();
$country->setCode('IS');
$contact = new Contact();
$contact->setCountry($country);
// $validator = Validation::createValidatorBuilder()->getValidator();
$validator = $this->createClient()->getContainer()->get('validator');
$errors = $validator->validate($contact, null, ['AppBundle']);
$this->assertEquals(1, count($errors));
}
And the testcase became green.
Hope this help
I have my custom module Customer feedback/Inquiry form in which customer can ask Inquiry related to product or they can able to give feedback related to store.In admin side i listed out all the feedbacks in admin grid.
Now I want to integrate the Mail functionality like when I click on particular feedback edit section there will be separate section for mail body where i will enter the reply, click on send button and mail goes to particular customer which mail Id has been already present in that particular edit section.
Here is code for my AdminHtml controller file
<?php
class Foo_Bar_Adminhtml_BazController extends Mage_Adminhtml_Controller_Action
{
public function indexAction()
{
// Let's call our initAction method which will set some basic params for each action
$this->_initAction()
->renderLayout();
}
public function newAction()
{
// We just forward the new action to a blank edit form
$this->_forward('edit');
}
public function editAction()
{
$this->_initAction();
// Get id if available
$id = $this->getRequest()->getParam('id');
$model = Mage::getModel('foo_bar/baz');
if ($id) {
// Load record
$model->load($id);
// Check if record is loaded
if (!$model->getId()) {
Mage::getSingleton('adminhtml/session')->addError($this->__('This baz no longer exists.'));
$this->_redirect('*/*/');
return;
}
}
$this->_title($model->getId() ? $model->getName() : $this->__('New Baz'));
$data = Mage::getSingleton('adminhtml/session')->getBazData(true);
if (!empty($data)) {
$model->setData($data);
}
Mage::register('foo_bar', $model);
$this->_initAction()
->_addBreadcrumb($id ? $this->__('Edit Baz') : $this->__('New Baz'), $id ? $this->__('Edit Baz') : $this->__('New Baz'))
->_addContent($this->getLayout()->createBlock('foo_bar/adminhtml_baz_edit')->setData('action', $this->getUrl('*/*/save')))
->renderLayout();
}
public function saveAction()
{
if ($postData = $this->getRequest()->getPost()) {
$model = Mage::getSingleton('foo_bar/baz');
$model->setData($postData);
try {
$model->save();
Mage::getSingleton('adminhtml/session')->addSuccess($this->__('The baz has been saved.'));
$this->_redirect('*/*/');
return;
}
catch (Mage_Core_Exception $e) {
Mage::getSingleton('adminhtml/session')->addError($e->getMessage());
}
catch (Exception $e) {
Mage::getSingleton('adminhtml/session')->addError($this->__('An error occurred while saving this baz.'));
}
Mage::getSingleton('adminhtml/session')->setBazData($postData);
$this->_redirectReferer();
}
}
public function deleteAction()
{
// check if we know what should be deleted
$itemId = $this->getRequest()->getParam('id');
if ($itemId) {
try {
// init model and delete
/** #var $model Magentostudy_News_Model_Item */
$model = Mage::getModel('foo_bar/baz');
$model->load($itemId);
if (!$model->getId()) {
Mage::throwException(Mage::helper('foo_bar')->__('Unable to find a Baz.'));
}
$model->delete();
// display success message
$this->_getSession()->addSuccess(
Mage::helper('foo_bar')->__('The Baz has been deleted.')
);
} catch (Mage_Core_Exception $e) {
$this->_getSession()->addError($e->getMessage());
} catch (Exception $e) {
$this->_getSession()->addException($e,
Mage::helper('foo_bar')->__('An error occurred while deleting the baz.')
);
}
}
// go to grid
$this->_redirect('*/*/');
}
public function messageAction()
{
$data = Mage::getModel('foo_bar/baz')->load($this->getRequest()->getParam('id'));
echo $data->getContent();
}
/**
* Initialize action
*
* Here, we set the breadcrumbs and the active menu
*
* #return Mage_Adminhtml_Controller_Action
*/
protected function _initAction()
{
$this->loadLayout()
// Make the active menu match the menu config nodes (without 'children' inbetween)
->_setActiveMenu('sales/foo_bar_baz')
->_title($this->__('Sales'))->_title($this->__('Baz'))
->_addBreadcrumb($this->__('Sales'), $this->__('Sales'))
->_addBreadcrumb($this->__('Baz'), $this->__('Baz'));
return $this;
}
/**
* Check currently called action by permissions for current user
*
* #return bool
*/
protected function _isAllowed()
{
return Mage::getSingleton('admin/session')->isAllowed('sales/foo_bar_baz');
}
}
I want some hooks from which i will able to send mail to particular customer.
Here is the image of my admin grid section
The most easiest way is to create a new transactional mail and set the subject to a placeholder and the same for the body.
this is the transactional mail function:
/**
* Send transactional email to recipient
*
* #param int $templateId
* #param string|array $sender sneder informatio, can be declared as part of config path
* #param string $email recipient email
* #param string $name recipient name
* #param array $vars varianles which can be used in template
* #param int|null $storeId
* #return Mage_Core_Model_Email_Template
*/
public function sendTransactional($templateId, $sender, $email, $name, $vars=array(), $storeId=null)
so the first to do is, create a new transactional mail under System->Transactional Mails. Just fill it with some random stuff for now. then
go to where ever you want to send the email and add
Mage::getModel('core/email_template')
->sendTransactional(
{the transactional email id we just created},
$sender,
$recepientEmail,
$recepientName,
array(
'subject' => '{your subject}',
'body' => '{you body}'
)
);
replace {your subject} and {your body} with the corresponding from you input fields.
after doing that go back to your transactional email template and replace our random stuff with that:
enter {{var subject}} in the subject field
and {{var body}} in die content field of the transactional mail
i didn't tried this but it should work.
hope that helps
I need to validate some variables values in Yii;
I dont have a model, and i need a pre build yii public method.
some of them must be integer, other string;
The values are being passed with GET.
I tryed all the validation classes that yii has and none works.
Has anyone tryed this and succeded ?
i need something like:
$validator = new CValidator();
$result = $validator->validate(array($key=>$value));
opened for sugestions
You can do it for specific validators:
$Validator = new CEmailValidator;
if($Validator->validateValue($value))
{
// Valid
}
From the Yii Framework file CEmailValidator.php:
/**
* Validates a static value to see if it is a valid email.
* Note that this method does not respect {#link allowEmpty} property.
* This method is provided so that you can call it directly without going through the model validation rule mechanism.
* #param mixed $value the value to be validated
* #return boolean whether the value is a valid email
* #since 1.1.1
*/
public function validateValue($value)
Yii validators are tightly integrated with models. So, atleast you need a dummy model object.
my suggestion would be like... create a dummy form model class..
class MyValidator extends CFormModel {
public function __get($name) {
return isset($_POST[$name])?$_POST[$name]:null;
}
static function myValidate( Array $rules ) {
$dummy = new MyValidator();
foreach($rules as $rule) {
if( isset($rule[0],$rule[1]) ) {
$validator = CValidator::createValidator(
$rule[1],
$dummy,
$rule[0],
array_slice($rule,2)
);
$validator->validate($dummy);
}
else { /* throw error; */ }
}
print_r( $dummy->getErrors() );
return !$dummy->hasErrors();
}
}
and use this myValidate static method anywhere just like below...
$rules = array(
array('name, email', 'required'),
array('email', 'email'),
);
if( MyValidator::myValidate($rules) ) {
....
}