Permalink
executable file 1157 lines (1003 sloc) 29.3 KB
<?php
namespace Illuminate\Validation;
use RuntimeException;
use BadMethodCallException;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Support\Fluent;
use Illuminate\Support\MessageBag;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Translation\Translator;
use Illuminate\Contracts\Validation\ImplicitRule;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Illuminate\Contracts\Validation\Rule as RuleContract;
use Illuminate\Contracts\Validation\Validator as ValidatorContract;
class Validator implements ValidatorContract
{
use Concerns\FormatsMessages,
Concerns\ValidatesAttributes;
/**
* The Translator implementation.
*
* @var \Illuminate\Contracts\Translation\Translator
*/
protected $translator;
/**
* The container instance.
*
* @var \Illuminate\Contracts\Container\Container
*/
protected $container;
/**
* The Presence Verifier implementation.
*
* @var \Illuminate\Validation\PresenceVerifierInterface
*/
protected $presenceVerifier;
/**
* The failed validation rules.
*
* @var array
*/
protected $failedRules = [];
/**
* The message bag instance.
*
* @var \Illuminate\Support\MessageBag
*/
protected $messages;
/**
* The data under validation.
*
* @var array
*/
protected $data;
/**
* The initial rules provided.
*
* @var array
*/
protected $initialRules;
/**
* The rules to be applied to the data.
*
* @var array
*/
protected $rules;
/**
* The current rule that is validating.
*
* @var string
*/
protected $currentRule;
/**
* The array of wildcard attributes with their asterisks expanded.
*
* @var array
*/
protected $implicitAttributes = [];
/**
* All of the registered "after" callbacks.
*
* @var array
*/
protected $after = [];
/**
* The array of custom error messages.
*
* @var array
*/
public $customMessages = [];
/**
* The array of fallback error messages.
*
* @var array
*/
public $fallbackMessages = [];
/**
* The array of custom attribute names.
*
* @var array
*/
public $customAttributes = [];
/**
* The array of custom displayable values.
*
* @var array
*/
public $customValues = [];
/**
* All of the custom validator extensions.
*
* @var array
*/
public $extensions = [];
/**
* All of the custom replacer extensions.
*
* @var array
*/
public $replacers = [];
/**
* The validation rules that may be applied to files.
*
* @var array
*/
protected $fileRules = [
'File', 'Image', 'Mimes', 'Mimetypes', 'Min',
'Max', 'Size', 'Between', 'Dimensions',
];
/**
* The validation rules that imply the field is required.
*
* @var array
*/
protected $implicitRules = [
'Required', 'Filled', 'RequiredWith', 'RequiredWithAll', 'RequiredWithout',
'RequiredWithoutAll', 'RequiredIf', 'RequiredUnless', 'Accepted', 'Present',
];
/**
* The validation rules which depend on other fields as parameters.
*
* @var array
*/
protected $dependentRules = [
'RequiredWith', 'RequiredWithAll', 'RequiredWithout', 'RequiredWithoutAll',
'RequiredIf', 'RequiredUnless', 'Confirmed', 'Same', 'Different', 'Unique',
'Before', 'After', 'BeforeOrEqual', 'AfterOrEqual', 'Gt', 'Lt', 'Gte', 'Lte',
];
/**
* The size related validation rules.
*
* @var array
*/
protected $sizeRules = ['Size', 'Between', 'Min', 'Max', 'Gt', 'Lt', 'Gte', 'Lte'];
/**
* The numeric related validation rules.
*
* @var array
*/
protected $numericRules = ['Numeric', 'Integer'];
/**
* Create a new Validator instance.
*
* @param \Illuminate\Contracts\Translation\Translator $translator
* @param array $data
* @param array $rules
* @param array $messages
* @param array $customAttributes
* @return void
*/
public function __construct(Translator $translator, array $data, array $rules,
array $messages = [], array $customAttributes = [])
{
$this->initialRules = $rules;
$this->translator = $translator;
$this->customMessages = $messages;
$this->data = $this->parseData($data);
$this->customAttributes = $customAttributes;
$this->setRules($rules);
}
/**
* Parse the data array, converting dots to ->.
*
* @param array $data
* @return array
*/
public function parseData(array $data)
{
$newData = [];
foreach ($data as $key => $value) {
if (is_array($value)) {
$value = $this->parseData($value);
}
// If the data key contains a dot, we will replace it with another character
// sequence so it doesn't interfere with dot processing when working with
// array based validation rules and array_dot later in the validations.
if (Str::contains($key, '.')) {
$newData[str_replace('.', '->', $key)] = $value;
} else {
$newData[$key] = $value;
}
}
return $newData;
}
/**
* Add an after validation callback.
*
* @param callable|string $callback
* @return $this
*/
public function after($callback)
{
$this->after[] = function () use ($callback) {
return call_user_func_array($callback, [$this]);
};
return $this;
}
/**
* Determine if the data passes the validation rules.
*
* @return bool
*/
public function passes()
{
$this->messages = new MessageBag;
// We'll spin through each rule, validating the attributes attached to that
// rule. Any error messages will be added to the containers with each of
// the other error messages, returning true if we don't have messages.
foreach ($this->rules as $attribute => $rules) {
$attribute = str_replace('\.', '->', $attribute);
foreach ($rules as $rule) {
$this->validateAttribute($attribute, $rule);
if ($this->shouldStopValidating($attribute)) {
break;
}
}
}
// Here we will spin through all of the "after" hooks on this validator and
// fire them off. This gives the callbacks a chance to perform all kinds
// of other validation that needs to get wrapped up in this operation.
foreach ($this->after as $after) {
call_user_func($after);
}
return $this->messages->isEmpty();
}
/**
* Determine if the data fails the validation rules.
*
* @return bool
*/
public function fails()
{
return ! $this->passes();
}
/**
* Run the validator's rules against its data.
*
* @return array
*
* @throws \Illuminate\Validation\ValidationException
*/
public function validate()
{
if ($this->fails()) {
throw new ValidationException($this);
}
$results = [];
$rules = collect($this->getRules())->keys()->map(function ($rule) {
return Str::contains($rule, '*') ? explode('.', $rule)[0] : $rule;
})->unique();
foreach ($rules as $rule) {
Arr::set($results, $rule, data_get($this->getData(), $rule));
}
return $results;
}
/**
* Validate a given attribute against a rule.
*
* @param string $attribute
* @param string $rule
* @return void
*/
protected function validateAttribute($attribute, $rule)
{
$this->currentRule = $rule;
list($rule, $parameters) = ValidationRuleParser::parse($rule);
if ($rule == '') {
return;
}
// First we will get the correct keys for the given attribute in case the field is nested in
// an array. Then we determine if the given rule accepts other field names as parameters.
// If so, we will replace any asterisks found in the parameters with the correct keys.
if (($keys = $this->getExplicitKeys($attribute)) &&
$this->dependsOnOtherFields($rule)) {
$parameters = $this->replaceAsterisksInParameters($parameters, $keys);
}
$value = $this->getValue($attribute);
// If the attribute is a file, we will verify that the file upload was actually successful
// and if it wasn't we will add a failure for the attribute. Files may not successfully
// upload if they are too large based on PHP's settings so we will bail in this case.
if ($value instanceof UploadedFile && ! $value->isValid() &&
$this->hasRule($attribute, array_merge($this->fileRules, $this->implicitRules))
) {
return $this->addFailure($attribute, 'uploaded', []);
}
// If we have made it this far we will make sure the attribute is validatable and if it is
// we will call the validation method with the attribute. If a method returns false the
// attribute is invalid and we will add a failure message for this failing attribute.
$validatable = $this->isValidatable($rule, $attribute, $value);
if ($rule instanceof RuleContract) {
return $validatable
? $this->validateUsingCustomRule($attribute, $value, $rule)
: null;
}
$method = "validate{$rule}";
if ($validatable && ! $this->$method($attribute, $value, $parameters, $this)) {
$this->addFailure($attribute, $rule, $parameters);
}
}
/**
* Determine if the given rule depends on other fields.
*
* @param string $rule
* @return bool
*/
protected function dependsOnOtherFields($rule)
{
return in_array($rule, $this->dependentRules);
}
/**
* Get the explicit keys from an attribute flattened with dot notation.
*
* E.g. 'foo.1.bar.spark.baz' -> [1, 'spark'] for 'foo.*.bar.*.baz'
*
* @param string $attribute
* @return array
*/
protected function getExplicitKeys($attribute)
{
$pattern = str_replace('\*', '([^\.]+)', preg_quote($this->getPrimaryAttribute($attribute), '/'));
if (preg_match('/^'.$pattern.'/', $attribute, $keys)) {
array_shift($keys);
return $keys;
}
return [];
}
/**
* Get the primary attribute name.
*
* For example, if "name.0" is given, "name.*" will be returned.
*
* @param string $attribute
* @return string
*/
protected function getPrimaryAttribute($attribute)
{
foreach ($this->implicitAttributes as $unparsed => $parsed) {
if (in_array($attribute, $parsed)) {
return $unparsed;
}
}
return $attribute;
}
/**
* Replace each field parameter which has asterisks with the given keys.
*
* @param array $parameters
* @param array $keys
* @return array
*/
protected function replaceAsterisksInParameters(array $parameters, array $keys)
{
return array_map(function ($field) use ($keys) {
return vsprintf(str_replace('*', '%s', $field), $keys);
}, $parameters);
}
/**
* Determine if the attribute is validatable.
*
* @param object|string $rule
* @param string $attribute
* @param mixed $value
* @return bool
*/
protected function isValidatable($rule, $attribute, $value)
{
return $this->presentOrRuleIsImplicit($rule, $attribute, $value) &&
$this->passesOptionalCheck($attribute) &&
$this->isNotNullIfMarkedAsNullable($rule, $attribute) &&
$this->hasNotFailedPreviousRuleIfPresenceRule($rule, $attribute);
}
/**
* Determine if the field is present, or the rule implies required.
*
* @param object|string $rule
* @param string $attribute
* @param mixed $value
* @return bool
*/
protected function presentOrRuleIsImplicit($rule, $attribute, $value)
{
if (is_string($value) && trim($value) === '') {
return $this->isImplicit($rule);
}
return $this->validatePresent($attribute, $value) ||
$this->isImplicit($rule);
}
/**
* Determine if a given rule implies the attribute is required.
*
* @param object|string $rule
* @return bool
*/
protected function isImplicit($rule)
{
return $rule instanceof ImplicitRule ||
in_array($rule, $this->implicitRules);
}
/**
* Determine if the attribute passes any optional check.
*
* @param string $attribute
* @return bool
*/
protected function passesOptionalCheck($attribute)
{
if (! $this->hasRule($attribute, ['Sometimes'])) {
return true;
}
$data = ValidationData::initializeAndGatherData($attribute, $this->data);
return array_key_exists($attribute, $data)
|| array_key_exists($attribute, $this->data);
}
/**
* Determine if the attribute fails the nullable check.
*
* @param string $rule
* @param string $attribute
* @return bool
*/
protected function isNotNullIfMarkedAsNullable($rule, $attribute)
{
if ($this->isImplicit($rule) || ! $this->hasRule($attribute, ['Nullable'])) {
return true;
}
return ! is_null(Arr::get($this->data, $attribute, 0));
}
/**
* Determine if it's a necessary presence validation.
*
* This is to avoid possible database type comparison errors.
*
* @param string $rule
* @param string $attribute
* @return bool
*/
protected function hasNotFailedPreviousRuleIfPresenceRule($rule, $attribute)
{
return in_array($rule, ['Unique', 'Exists']) ? ! $this->messages->has($attribute) : true;
}
/**
* Validate an attribute using a custom rule object.
*
* @param string $attribute
* @param mixed $value
* @param \Illuminate\Contracts\Validation\Rule $rule
* @return void
*/
protected function validateUsingCustomRule($attribute, $value, $rule)
{
if (! $rule->passes($attribute, $value)) {
$this->failedRules[$attribute][get_class($rule)] = [];
$this->messages->add($attribute, $this->makeReplacements(
$rule->message(), $attribute, get_class($rule), []
));
}
}
/**
* Check if we should stop further validations on a given attribute.
*
* @param string $attribute
* @return bool
*/
protected function shouldStopValidating($attribute)
{
if ($this->hasRule($attribute, ['Bail'])) {
return $this->messages->has($attribute);
}
if (isset($this->failedRules[$attribute]) &&
array_key_exists('uploaded', $this->failedRules[$attribute])) {
return true;
}
// In case the attribute has any rule that indicates that the field is required
// and that rule already failed then we should stop validation at this point
// as now there is no point in calling other rules with this field empty.
return $this->hasRule($attribute, $this->implicitRules) &&
isset($this->failedRules[$attribute]) &&
array_intersect(array_keys($this->failedRules[$attribute]), $this->implicitRules);
}
/**
* Add a failed rule and error message to the collection.
*
* @param string $attribute
* @param string $rule
* @param array $parameters
* @return void
*/
protected function addFailure($attribute, $rule, $parameters)
{
$this->messages->add($attribute, $this->makeReplacements(
$this->getMessage($attribute, $rule), $attribute, $rule, $parameters
));
$this->failedRules[$attribute][$rule] = $parameters;
}
/**
* Returns the data which was valid.
*
* @return array
*/
public function valid()
{
if (! $this->messages) {
$this->passes();
}
return array_diff_key(
$this->data, $this->attributesThatHaveMessages()
);
}
/**
* Returns the data which was invalid.
*
* @return array
*/
public function invalid()
{
if (! $this->messages) {
$this->passes();
}
return array_intersect_key(
$this->data, $this->attributesThatHaveMessages()
);
}
/**
* Generate an array of all attributes that have messages.
*
* @return array
*/
protected function attributesThatHaveMessages()
{
return collect($this->messages()->toArray())->map(function ($message, $key) {
return explode('.', $key)[0];
})->unique()->flip()->all();
}
/**
* Get the failed validation rules.
*
* @return array
*/
public function failed()
{
return $this->failedRules;
}
/**
* Get the message container for the validator.
*
* @return \Illuminate\Support\MessageBag
*/
public function messages()
{
if (! $this->messages) {
$this->passes();
}
return $this->messages;
}
/**
* An alternative more semantic shortcut to the message container.
*
* @return \Illuminate\Support\MessageBag
*/
public function errors()
{
return $this->messages();
}
/**
* Get the messages for the instance.
*
* @return \Illuminate\Support\MessageBag
*/
public function getMessageBag()
{
return $this->messages();
}
/**
* Determine if the given attribute has a rule in the given set.
*
* @param string $attribute
* @param string|array $rules
* @return bool
*/
public function hasRule($attribute, $rules)
{
return ! is_null($this->getRule($attribute, $rules));
}
/**
* Get a rule and its parameters for a given attribute.
*
* @param string $attribute
* @param string|array $rules
* @return array|null
*/
protected function getRule($attribute, $rules)
{
if (! array_key_exists($attribute, $this->rules)) {
return;
}
$rules = (array) $rules;
foreach ($this->rules[$attribute] as $rule) {
list($rule, $parameters) = ValidationRuleParser::parse($rule);
if (in_array($rule, $rules)) {
return [$rule, $parameters];
}
}
}
/**
* Get the data under validation.
*
* @return array
*/
public function attributes()
{
return $this->getData();
}
/**
* Get the data under validation.
*
* @return array
*/
public function getData()
{
return $this->data;
}
/**
* Set the data under validation.
*
* @param array $data
* @return $this
*/
public function setData(array $data)
{
$this->data = $this->parseData($data);
$this->setRules($this->initialRules);
return $this;
}
/**
* Get the value of a given attribute.
*
* @param string $attribute
* @return mixed
*/
protected function getValue($attribute)
{
return Arr::get($this->data, $attribute);
}
/**
* Get the validation rules.
*
* @return array
*/
public function getRules()
{
return $this->rules;
}
/**
* Set the validation rules.
*
* @param array $rules
* @return $this
*/
public function setRules(array $rules)
{
$this->initialRules = $rules;
$this->rules = [];
$this->addRules($rules);
return $this;
}
/**
* Parse the given rules and merge them into current rules.
*
* @param array $rules
* @return void
*/
public function addRules($rules)
{
// The primary purpose of this parser is to expand any "*" rules to the all
// of the explicit rules needed for the given data. For example the rule
// names.* would get expanded to names.0, names.1, etc. for this data.
$response = (new ValidationRuleParser($this->data))
->explode($rules);
$this->rules = array_merge_recursive(
$this->rules, $response->rules
);
$this->implicitAttributes = array_merge(
$this->implicitAttributes, $response->implicitAttributes
);
}
/**
* Add conditions to a given field based on a Closure.
*
* @param string|array $attribute
* @param string|array $rules
* @param callable $callback
* @return $this
*/
public function sometimes($attribute, $rules, callable $callback)
{
$payload = new Fluent($this->getData());
if (call_user_func($callback, $payload)) {
foreach ((array) $attribute as $key) {
$this->addRules([$key => $rules]);
}
}
return $this;
}
/**
* Register an array of custom validator extensions.
*
* @param array $extensions
* @return void
*/
public function addExtensions(array $extensions)
{
if ($extensions) {
$keys = array_map('\Illuminate\Support\Str::snake', array_keys($extensions));
$extensions = array_combine($keys, array_values($extensions));
}
$this->extensions = array_merge($this->extensions, $extensions);
}
/**
* Register an array of custom implicit validator extensions.
*
* @param array $extensions
* @return void
*/
public function addImplicitExtensions(array $extensions)
{
$this->addExtensions($extensions);
foreach ($extensions as $rule => $extension) {
$this->implicitRules[] = Str::studly($rule);
}
}
/**
* Register an array of custom implicit validator extensions.
*
* @param array $extensions
* @return void
*/
public function addDependentExtensions(array $extensions)
{
$this->addExtensions($extensions);
foreach ($extensions as $rule => $extension) {
$this->dependentRules[] = Str::studly($rule);
}
}
/**
* Register a custom validator extension.
*
* @param string $rule
* @param \Closure|string $extension
* @return void
*/
public function addExtension($rule, $extension)
{
$this->extensions[Str::snake($rule)] = $extension;
}
/**
* Register a custom implicit validator extension.
*
* @param string $rule
* @param \Closure|string $extension
* @return void
*/
public function addImplicitExtension($rule, $extension)
{
$this->addExtension($rule, $extension);
$this->implicitRules[] = Str::studly($rule);
}
/**
* Register a custom dependent validator extension.
*
* @param string $rule
* @param \Closure|string $extension
* @return void
*/
public function addDependentExtension($rule, $extension)
{
$this->addExtension($rule, $extension);
$this->dependentRules[] = Str::studly($rule);
}
/**
* Register an array of custom validator message replacers.
*
* @param array $replacers
* @return void
*/
public function addReplacers(array $replacers)
{
if ($replacers) {
$keys = array_map('\Illuminate\Support\Str::snake', array_keys($replacers));
$replacers = array_combine($keys, array_values($replacers));
}
$this->replacers = array_merge($this->replacers, $replacers);
}
/**
* Register a custom validator message replacer.
*
* @param string $rule
* @param \Closure|string $replacer
* @return void
*/
public function addReplacer($rule, $replacer)
{
$this->replacers[Str::snake($rule)] = $replacer;
}
/**
* Set the custom messages for the validator.
*
* @param array $messages
* @return $this
*/
public function setCustomMessages(array $messages)
{
$this->customMessages = array_merge($this->customMessages, $messages);
return $this;
}
/**
* Set the custom attributes on the validator.
*
* @param array $attributes
* @return $this
*/
public function setAttributeNames(array $attributes)
{
$this->customAttributes = $attributes;
return $this;
}
/**
* Add custom attributes to the validator.
*
* @param array $customAttributes
* @return $this
*/
public function addCustomAttributes(array $customAttributes)
{
$this->customAttributes = array_merge($this->customAttributes, $customAttributes);
return $this;
}
/**
* Set the custom values on the validator.
*
* @param array $values
* @return $this
*/
public function setValueNames(array $values)
{
$this->customValues = $values;
return $this;
}
/**
* Add the custom values for the validator.
*
* @param array $customValues
* @return $this
*/
public function addCustomValues(array $customValues)
{
$this->customValues = array_merge($this->customValues, $customValues);
return $this;
}
/**
* Set the fallback messages for the validator.
*
* @param array $messages
* @return void
*/
public function setFallbackMessages(array $messages)
{
$this->fallbackMessages = $messages;
}
/**
* Get the Presence Verifier implementation.
*
* @return \Illuminate\Validation\PresenceVerifierInterface
*
* @throws \RuntimeException
*/
public function getPresenceVerifier()
{
if (! isset($this->presenceVerifier)) {
throw new RuntimeException('Presence verifier has not been set.');
}
return $this->presenceVerifier;
}
/**
* Get the Presence Verifier implementation.
*
* @param string $connection
* @return \Illuminate\Validation\PresenceVerifierInterface
*
* @throws \RuntimeException
*/
protected function getPresenceVerifierFor($connection)
{
return tap($this->getPresenceVerifier(), function ($verifier) use ($connection) {
$verifier->setConnection($connection);
});
}
/**
* Set the Presence Verifier implementation.
*
* @param \Illuminate\Validation\PresenceVerifierInterface $presenceVerifier
* @return void
*/
public function setPresenceVerifier(PresenceVerifierInterface $presenceVerifier)
{
$this->presenceVerifier = $presenceVerifier;
}
/**
* Get the Translator implementation.
*
* @return \Illuminate\Contracts\Translation\Translator
*/
public function getTranslator()
{
return $this->translator;
}
/**
* Set the Translator implementation.
*
* @param \Illuminate\Contracts\Translation\Translator $translator
* @return void
*/
public function setTranslator(Translator $translator)
{
$this->translator = $translator;
}
/**
* Set the IoC container instance.
*
* @param \Illuminate\Contracts\Container\Container $container
* @return void
*/
public function setContainer(Container $container)
{
$this->container = $container;
}
/**
* Call a custom validator extension.
*
* @param string $rule
* @param array $parameters
* @return bool|null
*/
protected function callExtension($rule, $parameters)
{
$callback = $this->extensions[$rule];
if (is_callable($callback)) {
return call_user_func_array($callback, $parameters);
} elseif (is_string($callback)) {
return $this->callClassBasedExtension($callback, $parameters);
}
}
/**
* Call a class based validator extension.
*
* @param string $callback
* @param array $parameters
* @return bool
*/
protected function callClassBasedExtension($callback, $parameters)
{
list($class, $method) = Str::parseCallback($callback, 'validate');
return call_user_func_array([$this->container->make($class), $method], $parameters);
}
/**
* Handle dynamic calls to class methods.
*
* @param string $method
* @param array $parameters
* @return mixed
*
* @throws \BadMethodCallException
*/
public function __call($method, $parameters)
{
$rule = Str::snake(substr($method, 8));
if (isset($this->extensions[$rule])) {
return $this->callExtension($rule, $parameters);
}
throw new BadMethodCallException(sprintf(
'Method %s::%s does not exist.', static::class, $method
));
}
}