Better control on error message on Custom Laravel 5 validation - php

I am defining a Custom validator in my AppServiceProvider#boot as following
/* Custom unique value in set of fields validation */
Validator::extend('unique_in', function ($attribute, $value, $parameters, $validator) {
$validator_data = $validator->getData();
foreach ($parameters as $field) {
if ($value === array_get($validator_data, $field)) {
return false;
}
}
return true;
}, 'The :attribute cannot be same as any other field in this form.');
/* replace the :fields message string */
Validator::replacer('unique_in', function ($message, $attribute, $rule, $parameters) {
// I was doing this (this works)
// $message = str_replace(':field', implode(',',$parameters), $message);
// return $message;
//I want to do this (to get proper names for the fields)
$other = $this->getAttribute(array_shift($parameters));
return str_replace([':other', ':values'], [$other, implode(', ', $parameters)], $message);
});
Problem is instance of validator is not available to access getAttribute.
getAttribute resolves the readable name for parameters
Is there to access validator instance in replacer?
Note that the closure in Validator::extend has $validator which is an instance of validator.

I got it working using getCustomMessage and setCustomMessage, but without using Validator::replacer using Validator::extend alone.
Validator::extend('unique_in', function ($attribute, $value, $parameters, $validator) {
$validator_data = $validator->getData();
$parameters = array_diff($parameters, [$attribute]);
$same = null;
$other = null;
foreach ($parameters as $field) {
if ($value === array_get($validator_data, $field)) {
$same[] = $field;
}
}
//No same values found , validation success
if (is_null($same)) {
return true;
}
// Get all Custom Attributes those are defined for this validator and replace with field in $same array and create a new $other array
$custom_attributes = $validator->getCustomAttributes();
foreach ($same as $attribute) {
$other[$attribute] = isset($custom_attributes[$attribute]) ? $custom_attributes[$attribute] : $attribute;
}
//Task of Validator:replacer is done right here.
$message = 'The :attribute cannot have same value ":value" as in :fields';
$message = str_replace([':value',':fields'], [$value ,implode(', ', $other)], $message);
$validator->setCustomMessages(array_merge($validator->getCustomMessages(), ['unique_in' => $message]));
return false;
});
Thanks,
K

I know this is a bit old and already answered thread, but the first one I came about when searching for answers to this problem.
I would like to share my solution. I am using Localization with Laravel 5.4 in the Validator::replacer closure like this:
Validator::extend('empty_when', function ($attribute, $value, $parameters) {
foreach ($parameters as $key) {
if ( ! empty(Input::get($key))) {
return false;
}
}
return true;
});
Validator::replacer('empty_when', function ($message, $attribute, $rule, $parameters) {
$fields = [];
foreach ($parameters as $parameter) {
$fields[] = __('validation.attributes.'.$parameter);
}
return str_replace(':values', implode(', ', $fields), $message);
});

Use trans(). You can simply do like this:
Validator::replacer('greater_than', function($message, $attribute, $rule, $parameters) {
return str_replace(':field', trans('validation.attributes.'.$parameters[0]), $message);
});

Related

Laravel 5.3 Eloquent updateOrCreate

try {
\DB::beginTransaction();
Model::updateOrCreate( ['id' => $id, 'number' => $number],
['value' => $request->get('value')]
);
\DB::commit();
} catch (\Exception $e) {
\DB::rollBack();
throw new \Exception($e->error());
}
I was working on a task to create a common trait to prevent a record to be updated by multiple users at the same time. And, my method was to put hidden input of $data->updated_at in a blade and then to check it when an update request is sent. And there are some cases Laravel's updateOrCreate is used to update or create a new record. And, I don't know how to deal with that. Should, I split the methods to create and update or is there any good way to deal with it?
You probably want to separate what you are doing.
I was playing around and came up with this for fun (haven't tested):
Illuminate\Database\Eloquent\Builder::macro('createOrUpdateWhen', function ($attributes = [], $values = [], $when = null) {
$m = $this->firstOrNew($attributes);
if (is_array($when)) {
$when = function ($m) use ($when) {
foreach ($when as $key => $value) {
if ($m->$key != $value) {
return false;
}
}
return true;
}
}
if (! $m->exists || $when($m)) {
$m->fill($values);
}
$m->save();
return $m;
});
$m = M::createOrUpdateWhen($attributes, $values, function ($m) use ($request) {
return $m->updated_at == $request->input('updated_at');
});
$m = M::createOrUpdateWhen(
$attributes, $values, ['updated_at' => $request->input('updated_at')]
);
:-}
$post = Model::where([['id', $id], ['number', $number]])->first();
try {
\DB::beginTransaction();
if (is_null($post)) {
Model::create($input);
} else {
$post->updateWithExclLock($request->get('updated_at'), $input]);
}

Codeigniter Passing Extra Parameters to Custom Validation Rule

Based on this documentation , how to pass second parameter to the rule method?
This is my custom rule
public function email_exists($email, $exclude_id=NULL)
{
if ( $exclude_id !== NULL ) $this->db->where_not_in('id', $exclude_id);
$result = $this->db->select('id')->from('users')->where('email', $email)->get();
if ( $result->num_rows() > 0 ) {
$this->form_validation->set_message('email_exists', '{field} has been used by other user.');
return FALSE;
} else {
return TRUE;
}
}
and this is how i call it from controller
$rules = [
[
'field' => 'email',
'label' => 'Email',
'rules' => [
'required',
'trim',
'valid_email',
'xss_clean',
['email_exists', [$this->m_user, 'email_exists']]
]
]
];
$this->form_validation->set_rules($rules);
How can I pass second parameter to email_exists method?
Its seems CI does not provide a mechanism for this. I found several approaches to solve this. First way, you can hack the file system (Form_validation.php) and modify some script at line 728
if ( preg_match('/(.*?)\[(.*)\]/', $rule[1], $rulea) ) {
$method = $rulea[1];
$extra = $rulea[2];
} else {
$method = $rule[1];
$extra = NULL;
}
$result = is_array($rule)
? $rule[0]->{$method}($postdata, $extra)
: $rule($postdata);
Second way you can extends CI_Form_validation core and add your custom rule in it. I found the detail about this on codeigniter documentation.
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
class MY_Form_validation extends CI_Form_validation
{
public function __construct()
{
parent::__construct();
}
public function check_conflict_email($str, $exclude_id=NULL)
{
if ( $exclude_id !== NULL ) $this->CI->db->where_not_in('id', $exclude_id);
$result = $this->CI->db->select('id')->from('users')->where('email', $str)->get();
if ( $result->num_rows() > 0 ) {
$this->set_message('check_conflict_email', '{field} has been used by other user.');
return FALSE;
} else {
return TRUE;
}
}
}
/* End of file MY_Form_validation.php */
/* Location: ./application/libraries/MY_Form_validation.php */
Third way, and I think this is the best way to do it. Thanks to skunkbad for provide the solution
$rules = [
[
'field' => 'email',
'label' => 'Email',
'rules' => [
'required',
'trim',
'valid_email',
'xss_clean',
[
'email_exists',
function( $str ) use ( $second_param ){
return $this->m_user->email_exists( $str, $second_param );
}
]
]
]
];
Just do it the right way (at least for CI 2.1+) as described in the docs:
$this->form_validation->set_rules('uri', 'URI', 'callback_check_uri['.$this->input->post('id').']');
// Later:
function check_uri($field, $id){
// your callback code here
}
If this is not working than make an hidden field in your form for $exclude_id and check that directly in your callback via
$exclude_id = $this->input->post('exclude_id');//or whatever the field name is
More here
I use CI 3.1.10 and this issue still exists, I extend the library and use the same way as callback
array('username_callable[param]' => array($this->some_model, 'some_method'))
Extended Form_validation library:
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
class MY_Form_validation extends CI_Form_validation {
/**
* Executes the Validation routines
*
* #param array
* #param array
* #param mixed
* #param int
* #return mixed
*/
protected function _execute($row, $rules, $postdata = NULL, $cycles = 0)
{
// If the $_POST data is an array we will run a recursive call
//
// Note: We MUST check if the array is empty or not!
// Otherwise empty arrays will always pass validation.
if (is_array($postdata) && ! empty($postdata))
{
foreach ($postdata as $key => $val)
{
$this->_execute($row, $rules, $val, $key);
}
return;
}
$rules = $this->_prepare_rules($rules);
foreach ($rules as $rule)
{
$_in_array = FALSE;
// We set the $postdata variable with the current data in our master array so that
// each cycle of the loop is dealing with the processed data from the last cycle
if ($row['is_array'] === TRUE && is_array($this->_field_data[$row['field']]['postdata']))
{
// We shouldn't need this safety, but just in case there isn't an array index
// associated with this cycle we'll bail out
if ( ! isset($this->_field_data[$row['field']]['postdata'][$cycles]))
{
continue;
}
$postdata = $this->_field_data[$row['field']]['postdata'][$cycles];
$_in_array = TRUE;
}
else
{
// If we get an array field, but it's not expected - then it is most likely
// somebody messing with the form on the client side, so we'll just consider
// it an empty field
$postdata = is_array($this->_field_data[$row['field']]['postdata'])
? NULL
: $this->_field_data[$row['field']]['postdata'];
}
// Is the rule a callback?
$callback = $callable = FALSE;
if (is_string($rule))
{
if (strpos($rule, 'callback_') === 0)
{
$rule = substr($rule, 9);
$callback = TRUE;
}
}
elseif (is_callable($rule))
{
$callable = TRUE;
}
elseif (is_array($rule) && isset($rule[0], $rule[1]) && is_callable($rule[1]))
{
// We have a "named" callable, so save the name
$callable = $rule[0];
$rule = $rule[1];
}
// Strip the parameter (if exists) from the rule
// Rules can contain a parameter: max_length[5]
$param = FALSE;
if ( ! $callable && preg_match('/(.*?)\[(.*)\]/', $rule, $match))
{
$rule = $match[1];
$param = $match[2];
}
elseif ( is_string($callable) && preg_match('/(.*?)\[(.*)\]/', $callable, $match))
{
$param = $match[2];
}
// Ignore empty, non-required inputs with a few exceptions ...
if (
($postdata === NULL OR $postdata === '')
&& $callback === FALSE
&& $callable === FALSE
&& ! in_array($rule, array('required', 'isset', 'matches'), TRUE)
)
{
continue;
}
// Call the function that corresponds to the rule
if ($callback OR $callable !== FALSE)
{
if ($callback)
{
if ( ! method_exists($this->CI, $rule))
{
log_message('debug', 'Unable to find callback validation rule: '.$rule);
$result = FALSE;
}
else
{
// Run the function and grab the result
$result = $this->CI->$rule($postdata, $param);
}
}
else
{
$result = is_array($rule)
? $rule[0]->{$rule[1]}($postdata, $param)
: $rule($postdata);
// Is $callable set to a rule name?
if ($callable !== FALSE)
{
$rule = $callable;
}
}
// Re-assign the result to the master data array
if ($_in_array === TRUE)
{
$this->_field_data[$row['field']]['postdata'][$cycles] = is_bool($result) ? $postdata : $result;
}
else
{
$this->_field_data[$row['field']]['postdata'] = is_bool($result) ? $postdata : $result;
}
}
elseif ( ! method_exists($this, $rule))
{
// If our own wrapper function doesn't exist we see if a native PHP function does.
// Users can use any native PHP function call that has one param.
if (function_exists($rule))
{
// Native PHP functions issue warnings if you pass them more parameters than they use
$result = ($param !== FALSE) ? $rule($postdata, $param) : $rule($postdata);
if ($_in_array === TRUE)
{
$this->_field_data[$row['field']]['postdata'][$cycles] = is_bool($result) ? $postdata : $result;
}
else
{
$this->_field_data[$row['field']]['postdata'] = is_bool($result) ? $postdata : $result;
}
}
else
{
log_message('debug', 'Unable to find validation rule: '.$rule);
$result = FALSE;
}
}
else
{
$result = $this->$rule($postdata, $param);
if ($_in_array === TRUE)
{
$this->_field_data[$row['field']]['postdata'][$cycles] = is_bool($result) ? $postdata : $result;
}
else
{
$this->_field_data[$row['field']]['postdata'] = is_bool($result) ? $postdata : $result;
}
}
// Did the rule test negatively? If so, grab the error.
if ($result === FALSE)
{
// Callable rules might not have named error messages
if ( ! is_string($rule))
{
$line = $this->CI->lang->line('form_validation_error_message_not_set').'(Anonymous function)';
}
else
{
$line = $this->_get_error_message($rule, $row['field']);
}
// Is the parameter we are inserting into the error message the name
// of another field? If so we need to grab its "field label"
if (isset($this->_field_data[$param], $this->_field_data[$param]['label']))
{
$param = $this->_translate_fieldname($this->_field_data[$param]['label']);
}
// Build the error message
$message = $this->_build_error_msg($line, $this->_translate_fieldname($row['label']), $param);
// Save the error message
$this->_field_data[$row['field']]['error'] = $message;
if ( ! isset($this->_error_array[$row['field']]))
{
$this->_error_array[$row['field']] = $message;
}
return;
}
}
}
}

How to pass variables from validation rules to error message in laravel?

Here is the custom validation I made to validate a string. My question is how to pass a variable (i.e. missing_tags) into the error message? Thanks.
Validator::extend('tags', function($attribute, $value, $parameters, $validator) {
$tags = explode(',', $value);
$missing_tags = array()
foreach ($tags as $tag)
{
if (!Tag::exist($tag))
{
$missing_tags[] = $tag;
}
}
return count($missing_tags) == 0;
});
Validator::replacer('tags', function($message, $attribute, $rule, $parameters)
{
return str_replace('missing_tags', $missing_tags, $message);
});

Using $this when not in object context - Laravel 4

I have these two methods in my Contact.php model:
public function getSubscribers($listId)
{
return $this->withTrashed()
->where(DB::raw("concat('',email * 1)"), '!=', DB::raw('email'))
->where('opt_out', '0')
->select('email')
->chunk(1000, function($results) use ($listId) { $this->subscribeEmails($listId, $results); });
}
public function subscribeEmails($listId, $subscribers)
{
$emails = array();
foreach ($subscribers as $key => $subscriber)
{
$memberActivity = $subscriber->memberActivity($listId);
if ( ! $memberActivity['data'])
{
$emails[] = array('email' => $subscriber->email);
}
else
{
foreach ($memberActivity['data'] as $data)
{
foreach ($data['activity'] as $activity)
{
if ($activity['action'] !== 'unsub')
{
$emails[] = array('email' => $subscriber->email);
}
}
}
}
}
MailchimpWrapper::lists()->batchSubscribe($listId, $emails, false, true);
}
And the getSubscribers() method is called in my AdminContactsController.php controller via a method called updateMailchimp():
public function updateMailchimp()
{
$this->contact->getSubscribers($this->listId);
$message = (object) array(
'title' => 'Excellent!',
'content' => 'The Mailchimp newsletter list has been updated with the latest contacts from within the system.',
'alert_type' => 'success'
);
return Redirect::back()->with('message', $message);
}
Locally, this works great, no problems at all but on the staging server, I get the following error referencing the line cotaining ->chunk(1000, function($results) use ($listId) { $this->subscribeEmails($listId, $results); });:
Using $this when not in object context
Is this a PHP version issue or am I missing something here?
The reason why your code works on localhost but not on the remote server is probably the difference in PHP versions. Before PHP 5.4.0 it is not possible to use $this from anonymous function. You must pass the reference to $this within the use keyword:
public function getSubscribers($listId)
{
$that = $this; // <---- create reference to $this
return $this->withTrashed()
->where(DB::raw("concat('',email * 1)"), '!=', DB::raw('email'))
->where('opt_out', '0')
->select('email')
->chunk(1000, function($results) use (&$that, $listId) { $this->subscribeEmails($listId, $results); });
}

PHP: Modifying array with unknown structure at runtime; what is the most elegant solution?

PROBLEM
I have a function that takes in a nested array where the structure and nesting of the array is unknown at run-time. All that is known is some of the target fieldnames and desired values of some of the leafs.
QUESTIONS
1) I am hoping to modify this unknown structure and still have the code be readable and easily understood by fellow programmers. What (if any) solution will allow me to do things like this in PHP?
// Pseudo-code for things I would like to be able to do
// this is kinda like the same thing as XPATH, but for native PHP array
// find *every* fname with value of "Brad" and change it to "Brian"
$mydata->find_all('*:fname')->where_value_eq('Brad')->set_equal_to('Brian');
// find *the first* fave_color and set it to "Green"
$mydata->find('*:fave_color')->get(0)->set_equal_to('Green');
2) If there is nothing out there that will let me do this, is there something, anything, that at least comes close to the spirit of what I am hoping to accomplish here?
SAMPLE ARRAY
$mydata = array(
'people' => array(
array('fname'=>'Alice'),
array('fname'=>'Brad'),
array('fname'=>'Chris'),
),
'animals' => array(
array('fname'=>'Dino'),
array('fname'=>'Lassie'),
array('fname'=>'Brad'),
),
'settings' => array(
'user_prefs'=>array(
'localhost'=>array(
'fave_color'=>'blue',
),
),
),
'places' => array(
array('state'=>'New york',
'cities'=>array(
'name'=>'Albany',
'name'=>'Buffalo',
'name'=>'Corning',
),
'state'=>'California',
'cities'=>array(
'name'=>'Anaheim',
'name'=>'Bakersfield',
'name'=>'Carlsbad',
),
),
),
);
Although I maintain that you should stick with explicit manipulation as in my previous answer, boredom and intrigue got the better of me ;)
It probably has holes (and clearly lacks docs) but if you insist on this route, it should get you started:
class Finder {
protected $data;
public function __construct(&$data) {
if (!is_array($data)) {
throw new InvalidArgumentException;
}
$this->data = &$data;
}
public function all() {
return $this->find();
}
public function find($expression = null) {
if (!isset($expression)) {
return new Results($this->data);
}
$results = array();
$this->_find(explode(':', $expression), $this->data, $results);
return new Results($results);
}
protected function _find($parts, &$data, &$results) {
if (!$parts) {
return;
}
$currentParts = $parts;
$search = array_shift($currentParts);
if ($wildcard = $search == '*') {
$search = array_shift($currentParts);
}
foreach ($data as $key => &$value) {
if ($key === $search) {
if ($currentParts) {
$this->_find($currentParts, $value, $results);
} else {
$results[] = &$value;
}
} else if ($wildcard && is_array($value)) {
$this->_find($parts, $value, $results);
}
}
}
}
class Results {
protected $data;
public function __construct(&$data) {
$this->data = $data;
}
public function get($index, $limit = 1) {
$this->data = array_slice($this->data, $index, $limit);
return $this;
}
public function set_equal_to($value) {
foreach ($this->data as &$datum) {
$datum = $value;
}
}
public function __call($method, $args) {
if (!preg_match('/^where_?(key|value)_?(eq|contains)$/i', $method, $m)) {
throw new BadFunctionCallException;
}
if (!isset($args[0])) {
throw new InvalidArgumentException;
}
$operand = $args[0];
$isKey = strtolower($m[1]) == 'key';
$method = array('Results', '_compare' . (strtolower($m[2]) == 'eq' ? 'EqualTo' : 'Contains'));
$ret = array();
foreach ($this->data as $key => &$datum) {
if (call_user_func($method, $isKey ? $key : $datum, $operand)) {
$ret[] = &$datum;
}
}
$this->data = $ret;
return $this;
}
protected function _compareEqualTo($value, $test) {
return $value == $test;
}
protected function _compareContains($value, $test) {
return strpos($value, $test) !== false;
}
}
$finder = new Finder($mydata);
$finder->find('*:fname')->where_value_eq('Brad')->set_equal_to('Brian');
$finder->find('*:fave_color')->get(0)->set_equal_to('Green');
$finder->find('places:*:cities:*:name')->where_value_contains('ba')->set_equal_to('Stackoton');
print_r($mydata);
There's certainly no native solution for this and the syntax is rather strange. If you want the code to "be readable and easily understood by fellow programmers" please stick to methods that we're used to working with ;)
foreach ($mydata as $type => &$data) {
foreach ($data as &$member) {
if (isset($member['fname']) && $member['fname'] == 'Brad') {
$member['fname'] = 'Brian';
}
}
}
It's admittedly more verbose, but there's much less chance of confusion.

Categories