zf2 Form Collection Validation - Unique Elements in Fieldsets - php

I want to add unique Elements in a Zend Form Collection.
I found this awesome work from Aron Kerr
I do the forms and fieldsets like in Aron Kerr´s Example and it works fine.
In my case i create a Form to insert a collection of stores from a company.
My Form
First of all i have a Application\Form\CompanyStoreForm with a StoreFieldset like this:
$this->add(array(
'name' => 'company',
'type' => 'Application\Form\Stores\CompanyStoresFieldset',
));
The Fieldsets
Application\Form\Stores\CompanyStoresFieldset has a Collection of Store Entities like this:
$this->add(array(
'type' => 'Zend\Form\Element\Collection',
'name' => 'stores',
'options' => array(
'target_element' => array(
'type' => 'Application\Form\Fieldset\StoreEntityFieldset',
),
),
));
Application\Form\Fieldset\StoreEntityFieldset
$this->add(array(
'name' => 'storeName',
'attributes' => ...,
'options' => ...,
));
//AddressFieldset
$this->add(array(
'name' => 'address',
'type' => 'Application\Form\Fieldset\AddressFieldset',
));
The difference to Arron Kerrs CategoryFieldset is I adding one more fieldset: Application\Form\Fieldset\AddressFieldset
Application\Form\Fieldset\AddressFieldset has a text-element streetName.
The Inputfilters
The CompanyStoresFieldsetInputFilter has no elements to validate.
The StoreEntityFieldsetInputFilter has validators for storeName and the Application\Form\Fieldset\AddressFieldset like this
public function __construct() {
$factory = new InputFactory();
$this->add($factory->createInput([
'name' => 'storeName',
'required' => true,
'filters' => array( ....
),
'validators' => array(...
),
]));
$this->add(new AddressFieldsetInputFilter(), 'address');
}
The AddressFieldset has another Inputfilter AddressFieldsetInputFilter.
In AddressFieldsetInputFilter I adding a InputFilter for streetName.
FormFactory
Adding all Inputfilters to the Form like this
public function createService(ServiceLocatorInterface $serviceLocator) {
$form = $serviceLocator->get('FormElementManager')->get('Application\Form\CompanyStoreForm');
//Create a Form Inputfilter
$formFilter = new InputFilter();
//Create Inputfilter for CompanyStoresFieldsetInputFilter()
$formFilter->add(new CompanyStoresFieldsetInputFilter(), 'company');
//Create Inputfilter for StoreEntityFieldsetInputFilter()
$storeInputFilter = new CollectionInputFilter();
$storeInputFilter->setInputFilter(new StoreEntityFieldsetInputFilter());
$storeInputFilter->setUniqueFields(array('storeName'));
$storeInputFilter->setMessage('Just insert one entry with this store name.');
$formFilter->get('company')->add($storeInputFilter, 'stores');
$form->setInputFilter($formFilter);
return $form;
}
I use Aron Kerrs CollectionInputFilter.
The storeName should be unique in the whole collection.
All works fine, so far!
But now my problem!
The streetName should be unique in the whole collection.
But the Element is in the AddressFieldset.
I can´t do something like this:
$storeInputFilter->setUniqueFields(array('storeName', 'address' => array('streetName')));
I thought I should extend Aron Kerrs isValid() from CollectionInputFilter
Aron Kerrs Original isValid()
public function isValid()
{
$valid = parent::isValid();
// Check that any fields set to unique are unique
if($this->uniqueFields)
{
// for each of the unique fields specified spin through the collection rows and grab the values of the elements specified as unique.
foreach($this->uniqueFields as $k => $elementName)
{
$validationValues = array();
foreach($this->collectionValues as $rowKey => $rowValue)
{
// Check if the row has a deleted element and if it is set to 1. If it is don't validate this row.
if(array_key_exists('deleted', $rowValue) && $rowValue['deleted'] == 1) continue;
$validationValues[] = $rowValue[$elementName];
}
// Get only the unique values and then check if the count of unique values differs from the total count
$uniqueValues = array_unique($validationValues);
if(count($uniqueValues) < count($validationValues))
{
// The counts didn't match so now grab the row keys where the duplicate values were and set the element message to the element on that row
$duplicates = array_keys(array_diff_key($validationValues, $uniqueValues));
$valid = false;
$message = ($this->getMessage()) ? $this->getMessage() : $this::UNIQUE_MESSAGE;
foreach($duplicates as $duplicate)
{
$this->invalidInputs[$duplicate][$elementName] = array('unique' => $message);
}
}
}
return $valid;
}
}
First of all I try (just for testing) to add a error message to streetName in the first entry of the collection.
$this->invalidInputs[0]['address']['streetName'] = array('unique' => $message);
But it doens´t work.
Adding it to storeName it works
$this->invalidInputs[0]['storeName'] = array('unique' => $message);
I think the reason is the Fieldset has an own InputFilter()?
When i do a var_dump($this->collectionValues()) i received a multidimensional array of all values (also of the addressFieldset).
That´s fine! But i can´t add error messages to the element in the fieldset.
How can i do this?
I don´t want to insert all Elements of the AddressFieldset in the StoreEntityFieldset. (I use the AddressFieldset also in other Forms)

I figured it out. You simply can add values with
$this->invalidInputs[<entry-key>]['address']['streetName'] = array('unique' => $message);
I don´t know how it does not work yesterday. It was another bug.
I wrote a solution for my problem. Maybe it´s not the best, but it works for me.
CollectionInputFilter
class CollectionInputFilter extends ZendCollectionInputFilter
{
protected $uniqueFields;
protected $validationValues = array();
protected $message = array();
const UNIQUE_MESSAGE = 'Each item must be unique within the collection';
/**
* #return the $message
*/
public function getMessageByElement($elementName, $fieldset = null)
{
if($fieldset != null){
return $this->message[$fieldset][$elementName];
}
return $this->message[$elementName];
}
/**
* #param field_type $message
*/
public function setMessage($message)
{
$this->message = $message;
}
/**
* #return the $uniqueFields
*/
public function getUniqueFields()
{
return $this->uniqueFields;
}
/**
* #param multitype:string $uniqueFields
*/
public function setUniqueFields($uniqueFields)
{
$this->uniqueFields = $uniqueFields;
}
public function isValid()
{
$valid = parent::isValid();
// Check that any fields set to unique are unique
if($this->uniqueFields)
{
foreach($this->uniqueFields as $key => $elementOrFieldset){
// if the $elementOrFieldset is a fieldset, $key is our fieldset name, $elementOrFieldset is our collection of elements we have to check
if(is_array($elementOrFieldset) && !is_numeric($key)){
// We need to validate every element in the fieldset that should be unique
foreach($elementOrFieldset as $elementKey => $elementName){
// $key is our fieldset key, $elementName is the name of our element that should be unique
$validationValues = $this->getValidCollectionValues($elementName, $key);
// get just unique values
$uniqueValues = array_unique($validationValues);
//If we have a difference, not all are unique
if(count($uniqueValues) < count($validationValues))
{
// The counts didn't match so now grab the row keys where the duplicate values were and set the element message to the element on that row
$duplicates = array_keys(array_diff_key($validationValues, $uniqueValues));
$valid = false;
$message = ($this->getMessageByElement($elementName, $key)) ? $this->getMessageByElement($elementName, $key) : $this::UNIQUE_MESSAGE;
// set error messages
foreach($duplicates as $duplicate)
{
//$duplicate = our collection entry key, $key is our fieldsetname
$this->invalidInputs[$duplicate][$key][$elementName] = array('unique' => $message);
}
}
}
}
//its just a element in our collection, $elementOrFieldset is a simple element
else {
// in this case $key is our element key , we don´t need the second param because we haven´t a fieldset
$validationValues = $this->getValidCollectionValues($elementOrFieldset);
$uniqueValues = array_unique($validationValues);
if(count($uniqueValues) < count($validationValues))
{
// The counts didn't match so now grab the row keys where the duplicate values were and set the element message to the element on that row
$duplicates = array_keys(array_diff_key($validationValues, $uniqueValues));
$valid = false;
$message = ($this->getMessageByElement($elementOrFieldset)) ? $this->getMessageByElement($elementOrFieldset) : $this::UNIQUE_MESSAGE;
foreach($duplicates as $duplicate)
{
$this->invalidInputs[$duplicate][$elementOrFieldset] = array('unique' => $message);
}
}
}
}
}
return $valid;
}
/**
*
* #param type $elementName
* #param type $fieldset
* #return type
*/
public function getValidCollectionValues($elementName, $fieldset = null){
$validationValues = array();
foreach($this->collectionValues as $rowKey => $collection){
// If our values are in a fieldset
if($fieldset != null && is_array($collection[$fieldset])){
$rowValue = $collection[$fieldset][$elementName];
}
else{
//collection is one element like $key => $value
$rowValue = $collection[$elementName];
}
// Check if the row has a deleted element and if it is set to 1. If it is don't validate this row.
if($rowValue == 1 && $rowKey == 'deleted') continue;
$validationValues[$rowKey] = $rowValue;
}
return $validationValues;
}
public function getMessages()
{
$messages = array();
if (is_array($this->getInvalidInput()) || $this->getInvalidInput() instanceof Traversable) {
foreach ($this->getInvalidInput() as $key => $inputs) {
foreach ($inputs as $name => $input) {
if(!is_string($input) && !is_array($input))
{
$messages[$key][$name] = $input->getMessages();
continue;
}
$messages[$key][$name] = $input;
}
}
}
return $messages;
}
}
Define a CollectionInputFilter (in a factory)
$storeInputFilter = new CollectionInputFilter();
$storeInputFilter->setInputFilter(new StoreEntityFieldsetInputFilter());
$storeInputFilter->setUniqueFields(array('storeName', 'address' => array('streetName')));
$storeInputFilter->setMessage(array('storeName' => 'Just insert one entry with this store name.', 'address' => array('streetName' => 'You already insert a store with this street name')));
$formFilter->get('company')->add($storeInputFilter, 'stores');
So let me explain:
Now, we can add elements as unique in fieldsets in our collection.
We can NOT add collection fieldsets in our collection and not another fieldsets in our fieldsets.
In my opinion if anyone want to do this cases, they better should refactor the form :-)
setUniqueFields
Add a simple element as unique
array('your-unique-element','another-element');
If you want to add a element as unique in a fieldset
array('your-unique-element', 'fieldsetname' => array('your-unique-element-in-fieldset'))
We can add special messages for every element with setMessage
Add Message for a Element in the collection
array('storeName' => 'Just insert one entry...')
Add message for a Element in a fieldset
array('fieldset-name' => array('your-unique-element-in-fieldset' => 'You already insert ..'))

Related

Laravel form validation: same as field a or b

I have 3 fields: player1, player2 and winner. I want to validate that winner is equal to either player1 or player 2.
I tried looking for a way to add an 'or' clause to the validation but couldn't find anything, only workarounds for different problems.
You can use a custom validation rule to do this. There's nothing built in that allows comparison to one of many other fields. The closest is the same check, but it only checks against one other field.
(I've added dd() to dump data, you can remove them)
The $values will be what comes from your input.
The $validationRules can be adjusted for your needs.
Validator::extend('equals_one_of', function($attribute, $value, $parameters, \Illuminate\Validation\Validator $validator) {
$fields = $validator->getData(); // all posted values
foreach($parameters as $param) { // this is each of the comma separated fields in the validationRules array
if ($value == $fields[$param]) {
dd("matched");
return true;
}
}
dd("no match");
return false;
});
$values = [
'player1' => 'test1',
'player2' => 'test2',
'winner' => 'test1'
];
$validationRules = [
'player1' => 'required',
'player2' => 'required',
'winner' => 'required|equals_one_of:player1,player2'
];
$validate = Validator::make($values, $validationRules);
// use your validator as normal.
dd($validate->validate());
Actually this rule should be registered with extendDependent to correctly resolve parameter names when used with arrays.
Final code:
class OneOfRule
{
public function validate($attribute, $value, $parameters, $validator) {
if(is_null($value)) return true;
foreach($parameters as $param) {
$other = Arr::get($validator->getData(), $param);
if ($value === $other) {
return true;
}
}
return false;
}
}
public function boot()
{
Validator::extendDependent('one_of', 'App\Http\Validation\Rules\OneOfRule#validate');
}

Improving PHP loop validation

I have a php script that performs validation I have a code block that sets up 4 fields that need the same validation. What I want to achieve is that instead in this code block, I can setup the key name, the display name and field type and below set codes which would then automatically validate the fields according to the rules set for each field type.
Here is the code block in question:
// key name => display name
$fields = [
'firstName' => 'First Name',
'lastName' => 'Last Name',
'companyName' => 'Company Name',
'companyAddress' => 'Company Address',
];
So in this block I want to setup the key name, display name and field type. Currently I just got the these four fields. Is there a way I can achieve what I desire?
Here is my full code:
function validate($formData)
{
// Initiate Array
$validationMSG = array(); // array to hold validation errors
// what to validate (basics, i.e. required fields)
// key name => display name
$fields = [
'firstName' => 'First Name',
'lastName' => 'Last Name',
'companyName' => 'Company Name',
'companyAddress' => 'Company Address',
];
//simple loop
foreach($fields as $name => $display){
if(empty($formData[$name])){
$validationMSG[$name] = "${display} is required.";
}
}
//and NOW wee can perform some specific tests:
$pname_exp = '/^[a-zA-Z0-9\_]{2,20}/';
if(isset($formData['firstName']) && !preg_match($pname_exp, $formData['firstName'])){
$validationMSG['firstName'] = 'First Name is not valid.';
}
if(isset($formData['lastName']) && !preg_match($pname_exp, $formData['lastName'])){
$validationMSG['lastName'] = 'Last Name is required.';
}
//removed company name and company address checks, because we are done with them in the loop.
// Validate state
if (!isset($formData['state'])) {
$validationMSG['state'] = 'State is required.';
}
// Validate city
if (!isset($formData['city'])) {
$validationMSG['city'] = 'City is required.';
}
// Validate Zipcode - If Field is Empty
if (!isset($formData['zipcode'])) {
$validationMSG['zipcode'] = 'Zipcode is required.';
}
// Validate emailAddress
if (!isset($formData['emailAddress'])) {
$validationMSG['emailAddress'] = 'Email Address is required.';
}
// Check if emailAddress is a valid email address
elseif (!filter_var($formData['emailAddress'], FILTER_VALIDATE_EMAIL)) {
$validationMSG['emailAddress'] = 'Email address is not valid.';
}
//Validate phoneNumber
if (!isset($formData['phoneNumber'])) {
$validationMSG['phoneNumber'] = 'Phone Number is required.';
}
//Validate phoneNumber
elseif (preg_match('/^[0-9-\s]+$/D', $formData['phoneNumber'])) {
$validationMSG['phoneNumber'] = 'Must be a valid phone number.';
}
// Validate message
if (!isset($formData['message'])) {
$validationMSG['message'] = 'Message is required.';
}
if (!empty($validationMSG)) {
return $validationMSG;
}
else {
$captcha = checkCaptcha($formData['g-recaptcha-response']);
if(!$captcha['isSuccess']){
$validationMSG['captcha'] = 'ReCaptcha is required.';
return $validationMSG;
}
//End of Validation Function
}
}
//testing
$input = ['firstName' => 'John'];
$errors = validate($input);
var_dump($errors);
You should be using an approach like this. This is a starting point to write a better validation check in your loops:
<?php
function validate($formData)
{
// Initiate Array
$validationMSG = array(); // array to hold validation errors
// what to validate (basics, i.e. required fields)
// key name => display name
$fields = [
'firstName' => [
'label' => 'First Name',
'rules' => 'required'
],
'lastName' => [
'label' => 'Last Name',
'rules' => 'required'
],
'emailAddress' => [
'label' => 'Email',
'rules' => 'required|email'
]
];
//simple loop
foreach($fields as $fieldName => $args) {
$rules = explode('|', $args['rules']);
foreach($rules as $rule)
{
if($rule == 'required' && (!isset($formData[$fieldName]) || empty($formData[$fieldName])))
{
$validationMSG[$fieldName][] = sprintf('%s is a required field.', $args['label']);
}
if((isset($formData[$fieldName]) && $rule == 'email') && !filter_var($formData[$fieldName], FILTER_VALIDATE_EMAIL))
{
$validationMSG[$fieldName][] = sprintf('%s must be a valid email.', $args['label']);
}
}
}
return $validationMSG;
}
This can be improved but the concept will get you started.
Here is a way to do validation, while still having a format that can be promoted to a typed class (e.g., with namespace and dependency injection, etc) on a refactor. What I will show you is a way to acquire an object in such a way you can validate the message payload, based on a common naming construct.
First, create a regular php function called userActions(). This will be used to (globally or as an include where needed) call into and get these objects. This will return a closure.
function userActions(): \Closure { ... }
Next, you'll need to encapsulate your procedure. I recommend doing it like this:
$startUserRecord = static function(
string $firstName,
string $lastName,
string $companyName,
string $companyAddress
) {
return new class(...func_get_args()) {
private $payload = [];
private $firstName;
private $lastName;
private $companyName;
private $companyAddress;
public function __construct(
string $firstName,
string $lastName,
string $companyName,
string $companyAddress
) {
$this->payload = func_get_args();
$this->firstName = $firstName;
$this->lastName = $lastName;
$this->companyName = $companyName;
$this->companyAddress = $companyAddress;
}
public function validate(): array {
// Here is where you would actually validate this one message.
// Empty array means no validation messages (errors).
return [];
}
public function payload(): array {
return $this->payload;
}
};
};
// Other ones go here, too.
$saveUserProfile = static function() { ... };
$resetUserLogin = static function() { ... };
Corral them up to together, so we can use() them in our userActivities() closure:
$activities= [
'startUserRecord' => $startUserRecord,
'saveUserProfile ' => $saveUserProfile,
'resetUserLogin ' => $resetUserLogin,
];
Now, I've called it startUserRecord because I needed to make up a command name; give it the name you wish, however I recommend verb + subject + context construct, generally.
Static checks work for the "string, integer" and other low-level type checks, or class-based constructs as you have available (say, an Email value object). The validation of the values, as you can see, occurs inside the validate() method.
Next, create the closure that you'll return that actually allows you to search for and get only the command objects you want:
return static function(/* autoset as $get */) use($activities) {
$get = func_get_args();
$find = static function($name) use(&$get) {
return in_array($name, $get);
};
$filtered = array_filter($activities, $find, ARRAY_FILTER_USE_KEY);
$found = [];
// Keep in the same order.
foreach($get as $name) {
$found[] = $filtered[$name];
}
return $found;
};
Pay attention to the use() statement on the closure(s).
This is put within the userActivities() function declaration. Now, you can call and set it up, and generally use what you have:
$fields = [
'firstName' => 'First Name',
'lastName' => 'Last Name',
'companyName' => 'Company Name',
'companyAddress' => 'Company Address',
];
$userActivities = userActions();
[$startUserRecord] = $userActivities('startUserRecord');
$activity = $startUserRecord(...array_values($fields));
if ($errors = $activity->validate()) {
throw new InvalidCommand($errors);
}
// Do something with $activity->payload().
Note:
I'm not modifying the payload. You can add a conditioner/sanitizer method that acts on the payload or return the object's properties in some form.
We're destructuring with the [$startUserRecord] line. This allows you to return multiple entries and load them into their own variable names. An example:
[$saveUserProfile,$resetUserLogin] = $userActivites('saveUserProfile','resetUserLogin');
$startUserRecord(...array_values($fields)) This is called a spread or splat operation (splat operator: ... in the invocation), and makes each array item it's own separate argument, in order. I'm getting to that with array_values() here, however it's better to pass in an actual array without the keys, to maintain order in all cases.
Here it is put together: https://3v4l.org/sn61Q
This is just a starting point. Change it up, do what you need to, this should give you some idea what you can accomplish.
For instance, you could move the initializers to a with() method and "autoload" little closure-based validators that you share within the userActivities() function, like a $validateEmail() or $validatePhone() closures you declare and then use($validateEmail, ...) and within that closure new class($validateEmail, ...) to share these within an activity's context.

ZF2, pass variable to custom element from controller

In ZF2, I have a custom form element factory. It creates a custom MultiCheckbox and fills the checkbox values and labels from a db query.
class MyMultiCheckboxFactory
{
public function __invoke(FormElementManager $formElementManager)
{
$multiCheck = new \Zend\Form\Element\MultiCheckbox();
$serviceManager = $formElementManager->getServiceLocator();
$mapper = $serviceManager->get('Path\To\Mapper\To\Query\DB');
$descriptions = $mapper->findDescriptions($id);
// some processing to prepare $value_options array
$multiCheck->setOptions([
'label' => 'blah-blah',
'value_options' => $value_options
]);
return $multiCheck;
}
}
My problem is as follows. The method findDescriptions($id) depends on the $id which I can get from the route. But when I use MyMultiCheckbox in the form like this:
public function init()
{
$this->add([
'type' => 'Path\To\MyMultiCheckbox',
'name' => 'someName'
]);
}
I don't know how to pass the $id into the MyMultiCheckbox.
Could anyone help pleeeeeeeeeease?
You can fetch the id via the 'route match' instance inside the factory.
$event = $serviceManager->get('Application')->getMvcEvent();
$id = $event->getRouteMatch()->getParam('id', false);
if (empty($id)) {
throw new ServiceNotCreatedException('id not set!');
}
$descriptions = $mapper->findDescriptions($id);

Elasticsearch in laravel 5.1

I want to integrate elasticsearch in my laravel project.
I have installed using following line :
Run command on terminal :
composer require shift31/laravel-elasticsearch:~1.0
Then i have created elasticsearch.php in app/config/ and added following code.
<?php
use Monolog\Logger;
return array(
'hosts' => array(
'your.elasticsearch.server:9200' // what should be my host ?
),
'logPath' => 'path/to/your/elasticsearch/log',
'logLevel' => Logger::INFO
);
My first question : What should i write in place of host name
Right now my project is running on local server with localhost:8000.
I have added Shift31\LaravelElasticsearch\ElasticsearchServiceProvider in app/config/app.php for enable the 'Es' facade.
Above all things done. Now in which file i should add the code of elasticsearch to add, update, delete and search the records.
I have product table I need to add product records in elasticsearch, when update product, records should be update.
I have no idea of the further process. Please guide me I have searched on google but no any example help me.
Create the following helper classes in their respective paths:
App\Traits\ElasticSearchEventTrait.php
<?php
Namespace App\Traits;
trait ElasticSearchEventTrait {
public $esRemoveDefault = array('created_at','updated_at','deleted_at');
public static function boot()
{
parent::boot();
static::bootElasticSearchEvent();
}
public static function bootElasticSearchEvent()
{
static::created(function ($model) {
if(isset($model->esEnabled) && $model->esEnabled === true)
{
$model->esCreate();
}
});
static::updated(function ($model) {
if(isset($model->esEnabled) && $model->esEnabled === true)
{
$model->esUpdate();
}
});
static::deleted(function ($model) {
if(isset($model->esEnabled) && $model->esEnabled === true)
{
$model->esUpdate();
}
});
}
private function esCreate()
{
//esContext is false for polymorphic relations with no elasticsearch indexing
if(isset($this->esMain) && $this->esMain === true && $this->esContext !== false)
{
\Queue::push('ElasticSearchHelper#indexTask',array('id'=>$this->esGetId(),'class'=>get_class($this),'context'=>$this->esGetContext(),'info-context'=>$this->esGetInfoContext(),'excludes'=>$this->esGetRemove()));
}
else
{
$this->esUpdate();
}
}
private function esUpdate()
{
//esContext is false for polymorphic relations with no elasticsearch indexing
if($this->esContext !== false)
{
\Queue::push('ElasticSearchHelper#updateTask',array('id'=>$this->esGetId(),'class'=>get_class($this),'context'=>$this->esGetContext(),'info-context'=>$this->esGetInfoContext(),'excludes'=>$this->esGetRemove()));
}
}
/*
* Get Id of Model
*/
public function esGetId()
{
if(isset($this->esId))
{
return $this->esId;
}
else
{
return $this->id;
}
}
public function esGetInfoContext()
{
if(isset($this->esInfoContext))
{
return $this->esInfoContext;
}
else
{
throw new \RuntimeException("esInfoContext attribute or esGetInfoContext() is not set in class '".get_class($this)."'");
}
}
/*
* Name of main context of model
*/
public function esGetContext()
{
if(isset($this->esContext))
{
return $this->esContext;
}
else
{
throw new \RuntimeException("esContext attribute or esGetContext() method must be set in class '".get_class($this)."'");
}
}
/*
* All attributes that needs to be removed from model
*/
public function esGetRemove()
{
if(isset($this->esRemove))
{
return array_unique(array_merge($this->esRemoveDefault,$this->esRemove));
}
else
{
return $this->esRemoveDefault;
}
}
/*
* Extends Illuminate Collection to provide additional array functions
*/
public function newCollection(array $models = Array())
{
return new Core\Collection($models);
}
/**
* Return a timestamp as DateTime object.
*
* #param mixed $value
* #return \Carbon\Carbon
*/
public function asEsDateTime($value)
{
// If this value is an integer, we will assume it is a UNIX timestamp's value
// and format a Carbon object from this timestamp. This allows flexibility
// when defining your date fields as they might be UNIX timestamps here.
if (is_numeric($value))
{
return \Carbon::createFromTimestamp($value);
}
// If the value is in simply year, month, day format, we will instantiate the
// Carbon instances from that format. Again, this provides for simple date
// fields on the database, while still supporting Carbonized conversion.
elseif (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $value))
{
return \Carbon::createFromFormat('Y-m-d', $value)->startOfDay();
}
// Finally, we will just assume this date is in the format used by default on
// the database connection and use that format to create the Carbon object
// that is returned back out to the developers after we convert it here.
elseif ( ! $value instanceof DateTime)
{
$format = $this->getEsDateFormat();
return \Carbon::createFromFormat($format, $value);
}
return \Carbon::instance($value);
}
/**
* Get the format for database stored dates.
*
* #return string
*/
private function getEsDateFormat()
{
return $this->getConnection()->getQueryGrammar()->getDateFormat();
}
/*
* Converts model to a suitable format for ElasticSearch
*/
public function getEsSaveFormat()
{
$obj = clone $this;
//Go through ES Accessors
\ElasticSearchHelper::esAccessor($obj);
$dates = $this->getDates();
//Convert to array, then change Date to appropriate Elasticsearch format.
//Why? Because eloquent's date accessors is playing me.
$dataArray = $obj->attributesToArray();
//Remove all Excludes
foreach($this->esGetRemove() as $ex)
{
if(array_key_exists($ex,$dataArray))
{
unset($dataArray[$ex]);
}
}
if(!empty($dates))
{
foreach($dates as $d)
{
if(isset($dataArray[$d]) && $dataArray[$d] !== "" )
{
//Trigger Eloquent Getter which will provide a Carbon instance
$dataArray[$d] = $this->{$d}->toIso8601String();
}
}
}
return $dataArray;
}
}
App\Services\ElasticServiceHelper.php
<?php
/**
* Description of ElasticSearchHelper: Helps with Indexing/Updating with Elastic Search Server (https://www.elastic.co)
*
* #author kpudaruth
*/
Namespace App\Services;
class ElasticSearchHelper {
/*
* Laravel Queue - Index Task
* #param array $job
* #param array $data
*/
public function indexTask($job,$data)
{
if(\Config::get('website.elasticsearch') === true)
{
if(isset($data['context']))
{
$this->indexEs($data);
}
else
{
\Log::error('ElasticSearchHelper: No context set for the following dataset: '.json_encode($data));
}
}
$job->delete();
}
/*
* Laravel Queue - Update Task
* #param array $job
* #param array $data
*/
public function updateTask($job,$data)
{
if(\Config::get('website.elasticsearch') === true)
{
if(isset($data['context']))
{
$this->updateEs($data);
}
else
{
\Log::error('ElasticSearchHelper: No context set for the following dataset: '.json_encode($data));
}
}
$job->delete();
}
/*
* Index Elastic Search Document
* #param array $data
*/
public function indexEs($data)
{
$params = array();
$params['index'] = \App::environment();
$params['type'] = $data['context'];
$model = new $data['class'];
$form = $model::find($data['id']);
if($form)
{
$params['id'] = $form->id;
if($form->timestamps)
{
$params['timestamp'] = $form->updated_at->toIso8601String();
}
$params['body'][$data['context']] = $this->saveFormat($form);
\Es::index($params);
}
}
/*
* Update Elastic Search
* #param array $data
*/
public function updateEs($data)
{
$params = array();
$params['index'] = \App::environment();
$params['type'] = $data['context'];
$model = new $data['class'];
$form = $model::withTrashed()->find($data['id']);
if(count($form))
{
/*
* Main form is being updated
*/
if($data['info-context'] === $data['context'])
{
$params['id'] = $data['id'];
$params['body']['doc'][$data['info-context']] = $this->saveFormat($form);
}
else
{
//Form is child, we get parent
$parent = $form->esGetParent();
if(count($parent))
{
//Id is always that of parent
$params['id'] = $parent->id;
//fetch all children, given that we cannot save per children basis
$children = $parent->{$data['info-context']}()->get();
if(count($children))
{
//Get data in a format that can be saved by Elastic Search
$params['body']['doc'][$data['info-context']] = $this->saveFormat($children);
}
else
{
//Empty it is
$params['body']['doc'][$data['info-context']] = array();
}
}
else
{
\Log::error("Parent not found for {$data['context']} - {$data['class']}, Id: {$data['id']}");
return false;
}
}
//Check if Parent Exists
try
{
$result = \Es::get([
'id' => $params['id'],
'index' => $params['index'],
'type' => $data['context']
]);
} catch (\Exception $ex) {
if($ex instanceof \Elasticsearch\Common\Exceptions\Missing404Exception || $ex instanceof \Guzzle\Http\Exception\ClientErrorResponseException)
{
//if not, we set it
if (isset($parent) && $parent)
{
$this->indexEs([
'context' => $data['context'],
'class' => get_class($parent),
'id' => $parent->id,
]);
}
else
{
\Log::error('Unexpected error in updating elasticsearch records, parent not set with message: '.$ex->getMessage());
return false;
}
}
else
{
\Log::error('Unexpected error in updating elasticsearch records: '.$ex->getMessage());
return false;
}
}
\Es::update($params);
}
}
/*
* Iterate through all Es accessors of the model.
* #param \Illuminate\Database\Eloquent\Model $object
*/
public function esAccessor(&$object)
{
if(is_object($object))
{
$attributes = $object->getAttributes();
foreach($attributes as $name => $value)
{
$esMutator = 'get' . studly_case($name) . 'EsAttribute';
if (method_exists($object, $esMutator)) {
$object->{$name} = $object->$esMutator($object->{$name});
}
}
}
else
{
throw New \RuntimeException("Expected type object");
}
}
/*
* Iterates over a collection applying the getEsSaveFormat function
* #param mixed $object
*
* #return array
*/
public function saveFormat($object)
{
if($object instanceof \Illuminate\Database\Eloquent\Model)
{
return $object->getEsSaveFormat();
}
else
{
return array_map(function($value)
{
return $value->getEsSaveFormat();
}, $object->all());
}
}
}
A couple of gotchas from the above helper classes:
The default ElasticSearch index is set to the name of the App's Environment
The ..task() functions are meant for the old laravel 4.2 queue format. I've yet to port those to laravel 5.x. Same goes for the Queue::push commands.
Example
ElasticSearch Mapping:
[
'automobile' => [
"dynamic" => "strict",
'properties' => [
'automobile' => [
'properties' => [
'id' => [
'type' => 'long',
'index' => 'not_analyzed'
],
'manufacturer_name' => [
'type' => 'string',
],
'manufactured_on' => [
'type' => 'date'
]
]
],
'car' => [
'properties' => [
'id' => [
'type' => 'long',
'index' => 'not_analyzed'
],
'name' => [
'type' => 'string',
],
'model_id' => [
'type' => 'string'
]
]
],
"car-model" => [
'properties' => [
'id' => [
'type' => 'long',
'index' => 'not_analyzed'
],
'description' => [
'type' => 'string',
],
'name' => [
'type' => 'string'
]
]
]
]
]
]
Top level document is called 'automobile'. Underneath it, you have 'automobile', 'car' & 'car-model'. Consider 'car' & 'car-model' as relations to the automobile. They are known as sub documents on elasticsearch. (See: https://www.elastic.co/guide/en/elasticsearch/guide/current/document.html)
Model: App\Models\Car.php
namespace App\Models;
class Car extends \Eloquent {
use \Illuminate\Database\Eloquent\SoftDeletingTrait;
use \App\Traits\ElasticSearchEventTrait;
protected $table = 'car';
protected $fillable = [
'name',
'serie',
'model_id',
'automobile_id'
];
protected $dates = [
'deleted_at'
];
/* Elastic Search */
//Indexing Enabled
public $esEnabled = true;
//Context for Indexing - Top Level name in the mapping
public $esContext = "automobile";
//Info Context - Secondary level name in the mapping.
public $esInfoContext = "car";
//The following fields will not be saved in elasticsearch.
public $esRemove = ['automobile_id'];
//Fetches parent relation of car, so that we can retrieve its id for saving in the appropriate elasticsearch record
public function esGetParent()
{
return $this->automobile;
}
/*
* Event Observers
*/
public static function boot() {
parent:: boot();
//Attach events to model on start
static::bootElasticSearchEvent();
}
/*
* ElasticSearch Accessor
*
* Sometimes you might wish to format the data before storing it in elasticsearch,
* The accessor name is in the format of: get + attribute's name camel case + EsAttribute
* The $val parameter will always be the value of the attribute that is being accessed.
*
* #param mixed $val
*/
/*
* Elasticsearch Accessor: Model Id
*
* Get the model name and save it
*
* #param int $model_id
* #return string
*/
public function getModelIdEsAttribute($model_id) {
//Fetch model from table
$model = \App\Models\CarModel::find($model_id);
if($model) {
//Return name of model if found
return $model->name;
} else {
return '';
}
}
/*
* Automobile Relationship: Belongs To
*/
public function automobile()
{
return $this->belongsTo('\App\Models\Automobile','automobile_id');
}
}
Example of Search Query:
/**
* Get search results
*
* #param string $search (Search string)
*
*/
public function getAll($search)
{
$params = array();
$params['index'] = App::environment();
//Declare your mapping names in the array which you wish to search on.
$params['type'] = array('automobile');
/*
* Build Query String
*/
//Exact match is favored instead of fuzzy ones
$params['body']['query']['bool']['should'][0]['match']['name']['query'] = $search;
$params['body']['query']['bool']['should'][0]['match']['name']['operator'] = "and";
$params['body']['query']['bool']['should'][0]['match']['name']['boost'] = 2;
$params['body']['query']['bool']['should'][1]['fuzzy_like_this']['like_text'] = $search;
$params['body']['query']['bool']['should'][1]['fuzzy_like_this']['fuzziness'] = 0.5;
$params['body']['query']['bool']['should'][1]['fuzzy_like_this']['prefix_length'] = 2;
$params['body']['query']['bool']['minimum_should_match'] = 1;
//Highlight matches
$params['body']['highlight']['fields']['*'] = new \stdClass();
$params['body']['highlight']['pre_tags'] = array('<b>');
$params['body']['highlight']['post_tags'] = array('</b>');
//Exclude laravel timestamps
$params['body']['_source']['exclude'] = array( "*.created_at","*.updated_at","*.deleted_at");
/*
* Poll search server until we have some results
*/
$from_offset = 0;
$result = array();
//Loop through all the search results
do
{
try
{
$params['body']['from'] = $from_offset;
$params['body']['size'] = 5;
$queryResponse = \Es::search($params);
//Custom function to process the result
//Since we will receive a bunch of arrays, we need to reformat the data and display it properly.
$result = $this->processSearchResult($queryResponse);
$from_offset+= 5;
}
catch (\Exception $e)
{
\Log::error($e->getMessage());
return Response::make("An error occured with the search server.",500);
}
}
while (count($result) === 0 && $queryResponse['hits']['total'] > 0);
echo json_encode($result);
}
/*
* Format search results as necessary
* #param array $queryResponse
*/
private function processSearchResult(array $queryResponse)
{
$result = array();
//Check if we have results in the array
if($queryResponse['hits']['total'] > 0 && $queryResponse['timed_out'] === false)
{
//Loop through each result
foreach($queryResponse['hits']['hits'] as $line)
{
//Elasticsearch will highlight the relevant sections in your query in an array. The below creates a readable format with · as delimiter.
$highlight = "";
if(isset($line['highlight']))
{
foreach($line['highlight'] as $k=>$v)
{
foreach($v as $val)
{
$highlight[] = str_replace("_"," ",implode(" - ",explode(".",$k)))." : ".$val;
}
}
$highlight = implode(" · ",$highlight);
}
//Check the mapping type
switch($line['_type'])
{
case "automobile":
$result[] = array('icon'=>'fa-automobile',
'title'=> 'Automobile',
'id' => $line['_id'],
//name to be displayed on my search result page
'value'=>$line['_source'][$line['_type']]['name']." (Code: ".$line['_id'].")",
//Using a helper to generate the url. Build your own class.
'url'=>\App\Helpers\URLGenerator::generate($line['_type'],$line['_id']),
//And the highlights as formatted above.
'highlight'=>$highlight);
break;
}
}
}
return $result;
}

How to create a step by step form with Phalcon in PHP

I'm currently working on a project using the Phalcon Framework that has pages with complex forms and a lot of inputs, to break it down nicely I'm dividing the forms into a step-by-step process.
How would one validate the form on each step before going to the next step and then save the whole form on the final step?
I can't seem to find anything documented about this sort of process as it likes to validate the form in it's entirety if I use the form builder.
Simple, just create a custom methods in your form class to validate any step, and the posted data from some step save into message class and store it into session by "stepX", when posted data is not valid just set defaults from post. When valid save it into session as i describe above.
For example how i mean "controller"
<?php
class MyController extends BaseController {
public function processStep1Action(){
$form = new MyForm();
if($this->request->isPost()){//im using my custom request class
if(!$form->isValid($this->request->getPost()){
//error messages goes here
$form->setDefaultsFromRequest($this->request); // it will set the filled data
}
else {
$messageClass = new MyMessageContainer();
$messageClass->setData($this->request);//inside parse requested data into message class, or parse it as $messageClass->name = $this->request->getPost('name');
$this->session->save('step1',$messageClass); //maybe it would be want to serialize it
//then redirect to the step 2 or x
}
}
}
}
So in the next step you can access data from sessions $this->session->get('step1'); so you can in final step load all posted data and store it into DB.
I hope this helps! :)
here is my form maybe it can be helpful for you.
<?php
namespace Manager\Library\Forms\User;
use Phalcon\Forms\Form,
Phalcon\Forms\Element\Email,
Phalcon\Forms\Element\Select,
Phalcon\Forms\Element\Password,
Phalcon\Forms\Element\Check,
Phalcon\Validation\Validator\Confirmation,
Phalcon\Validation\Validator\StringLength,
Phalcon\Forms\Element\Submit,
Phalcon\Validation\Validator\PresenceOf,
Model\Group;
class AddUser extends Form {
public function initialize()
{
$email = new Email('email');
$email->addValidators(array(
new \Phalcon\Validation\Validator\Email(array(
'message' => 'Nezadali jste email nebo má nesprávny tvar(email#domena.tld).'
))
));
$this->add($email);
$this->initGroupElement();
$password = new Password('password');
$password
->addValidator(new StringLength(array('min' => 6,'messageMinimum' => 'Nezadali jste heslo nebo je příliš krátke, minimální počet znaků je 6.')))
->addValidator(new Confirmation(array('with' => 'password-again',"message" => "Zadané hesla se neshodují.")));
$this->add($password);
$repeatPassword = new Password('password-again');
$this->add($repeatPassword);
$this->initializeProfileElements();
$active = new Check('active',array('value' => 1));
$this->add($active);
$this->add( new Submit('save') );
\Phalcon\Tag::setDefault('password', '');
\Phalcon\Tag::setDefault('password-again', '');
}
public function initializeEdit(){
$email = new Email('email');
$email->addValidators(array(
new \Phalcon\Validation\Validator\Email(array(
'message' => 'Nezadali jste email nebo má nesprávny tvar(email#domena.tld).'
))
));
$this->add($email);
$this->initGroupElement();
$password = new Password('password');
$this->add($password);
$repeatPassword = new Password('password-again');
$this->add($repeatPassword);
$this->initializeProfileElements();
$active = new Check('active',array('value' => 1));
$this->add($active);
$this->add( new Submit('save') );
\Phalcon\Tag::setDefault('password', '');
\Phalcon\Tag::setDefault('password-again', '');
}
protected function initGroupElement(){
$auth = \Core\Auth::getIdentity();
$groups = new Group();
// $groups->addColumns(array('id','name'));
//set global condition about Super Admin
$groups->addFilter('id', 1,'<>');
if($auth){
//set restrictions for main groups
if((int)$auth->group_id === 1){ //super admingroup
//no filter
}
else if((int)$auth->group_id === 2){ //admin group
$groups->addFilter('id', 1,'>');
}
else if((int)$auth->group_id === 6){//Provozovatel group
$groups->addFilter('id',array(3,6,7));
$groups->addFilter('public', 1,'=',true);
}
else { // other groups
$groups->addFilter('public', 1);
}
}
$groups = $groups->findFiltered();
$groupElement = new Select('group');
foreach($groups as $group){
$groupElement->addOption(array($group->id => $group->name));
}
$this->add($groupElement);
}
protected function initializeProfileElements(){
$forename = new \Phalcon\Forms\Element\Text('forename');
$this->add($forename);
$surname = new \Phalcon\Forms\Element\Text('surname');
$this->add($surname);
$street = new \Phalcon\Forms\Element\Text('street');
$this->add($street);
$postal = new \Phalcon\Forms\Element\Text('postal');
$this->add($postal);
$city = new \Phalcon\Forms\Element\Text('city');
$this->add($city);
$ic = new \Phalcon\Forms\Element\Text('ic');
$this->add($ic);
$dic = new \Phalcon\Forms\Element\Text('dic');
$this->add($dic);
}
public function setDefault($fieldName,$value){
\Phalcon\Tag::setDefault($fieldName, $value);
}
public function setDefaults($object){
if($object instanceof \Model\User){
$this->setDefaultsFromObject($object);
}
else if($object instanceof \Phalcon\Http\Request){
$this->setDefaultsFromRequest($object);
}
}
protected function setDefaultsFromObject(\Model\User $user){
$profile = $user->getRelated('\Model\Profile');
\Phalcon\Tag::setDefaults(array(
'email' => $user->email,
'group' => $user->group_id,
'active' => $user->active,
'forename' => $profile->forename,
'surname' => $profile->surname,
'street' => $profile->street,
'city' => $profile->city,
'postal' => $profile->postal,
'ic' => $profile->IC,
'dic' => $profile->DIC
));
}
protected function setDefaultsFromRequest(\Phalcon\Http\Request $request){
\Phalcon\Tag::setDefaults(array(
'email' => $request->getPost('email'),
'group' => $request->getPost('group'),
'active' => $request->getPost('active')
));
\Phalcon\Tag::setDefaults(array(
'forename' => $request->getPost('forename'),
'surname' => $request->getPost('surname'),
'street' => $request->getPost('street'),
'city' => $request->getPost('city'),
'postal' => $request->getPost('postal'),
'ic' => $request->getPost('ic'),
'dic' => $request->getPost('dic')
));
}
}
In addition to Kamil's answer, another option to consider is to use Javascript on the front-end to handle your multi-step form. This will add some complexity as you will need to have the javascript to handle the form steps and do preliminary validation, but it only requires a single submit where you can validate content within a single method.

Categories