Skip to content

Instantly share code, notes, and snippets.

@kmuenkel
Created November 24, 2020 17:27
Show Gist options
  • Save kmuenkel/aff413e7e14051bcdb49c682cedf8eb8 to your computer and use it in GitHub Desktop.
Save kmuenkel/aff413e7e14051bcdb49c682cedf8eb8 to your computer and use it in GitHub Desktop.
Laravel Validator Fix
<?php
namespace App\Providers;
use ReflectionException;
use Illuminate\Validation\Validator;
use App\Validators\Rules\CustomRule;
use Illuminate\Support\ServiceProvider;
use Illuminate\Contracts\Validation\Rule;
use Lti\Overrides\Validation\Validator as ValidatorOverride;
use Illuminate\Support\Facades\Validator as ValidatorFacade;
use Illuminate\Contracts\Validation\Factory as ValidationFactory;
/**
* Class LtiServiceProvider
* @package Lti\Providers
*/
class LtiServiceProvider extends ServiceProvider
{
/**
* @void
* @throws ReflectionException
*/
public function boot()
{
$this->loadCustomValidationRules(['ruleName' => CustomRule::class]);
$this->overrideValidator();
}
/**
* @void
*/
protected function overrideValidator()
{
$validatorFactory = app(ValidationFactory::class);
$validatorFactory->resolver(function (...$args) {
return new ValidatorOverride(...$args);
});
}
/**
* @param string[] $rules
*/
protected function loadCustomValidationRules(array $rules)
{
foreach ($rules as $name => $ruleClass) {
//Logically, a string by-reference variable should work for this, but it's disallowed in this context
$message = new class {
/**
* @var Validator|null
*/
public static $validator;
/**
* @var Rule|null
*/
public static $rule;
/**
* @var string[]
*/
public static $parameters = [];
/**
* @var mixed
*/
public static $attribute = null;
/**
* @var string
*/
public static $ruleName = '';
/**
* @return string
*/
public function __toString()
{
return static::$validator->makeReplacements(
static::$rule->message(),
static::$attribute,
static::$ruleName,
static::$parameters
);
}
};
$message::$rule = $custom = app($ruleClass);
$instance = null;
//Intercept the arguments passed to the Rule::passes() method
$extension = function ($attribute, $value, $parameters, Validator $validator) use ($custom, $message) {
$message::$validator = $validator;
$message::$attribute = $attribute;
$message::$parameters = $parameters;
return $custom->passes($attribute, $value, $parameters, $validator);
};
//ValidatorFacade::replacer() would result in infinite recursion when using Validator::makeReplacements()
ValidatorFacade::extend($message::$ruleName = $name, $extension, $message);
}
}
}
<?php
namespace App\Overrides\Validation;
use Illuminate\Support\Arr;
use Illuminate\Validation\ValidationData as BaseValidationData;
/**
* Class ValidationData
* @package Lti\Overrides\Validation
*/
class ValidationData extends BaseValidationData
{
/**
* Correct the absence of the preg_quote() $delimiter
* @inheritDoc
*/
protected static function extractValuesForWildcards($masterData, $data, $attribute)
{
$keys = [];
$pattern = str_replace('\*', '[^\.]+', preg_quote($attribute, '/'));
foreach ($data as $key => $value) {
try {
if ((bool)preg_match('/^' . $pattern . '/', $key, $matches)) {
$keys[] = $matches[0];
}
} catch (\Exception $e) {
dd($pattern);
}
}
$keys = array_unique($keys);
$data = [];
foreach ($keys as $key) {
$data[$key] = Arr::get($masterData, $key);
}
return $data;
}
}
<?php
namespace App\Overrides\Validation;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationRuleParser as BaseValidationRuleParser;
/**
* Class ValidationRuleParser
* @package Lti\Overrides\Validation
*/
class ValidationRuleParser extends BaseValidationRuleParser
{
/**
* Correct the absence of the preg_quote() $delimiter
* @inheritDoc
*/
protected function explodeWildcardRules($results, $attribute, $rules)
{
$pattern = str_replace('\*', '[^\.]*', preg_quote($attribute, '/'));
$data = ValidationData::initializeAndGatherData($attribute, $this->data);
foreach ($data as $key => $value) {
if (Str::startsWith($key, $attribute) || (bool) preg_match('/^'.$pattern.'\z/', $key)) {
foreach ((array) $rules as $rule) {
$this->implicitAttributes[$attribute][] = $key;
$results = $this->mergeRules($results, $key, $rule);
}
}
}
return $results;
}
}
<?php
namespace App\Overrides\Validation;
use DateTime;
use Illuminate\Validation\Validator as BaseValidator;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Illuminate\Contracts\Validation\Rule as RuleContract;
/**
* Class Validator
* @package Lti\Overrides\Validation
*/
class Validator extends BaseValidator
{
/**
* Leverage the ValidationData override
* @inheritDoc
*/
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);
}
/**
* Leverage the ValidationRuleParser override
* @inheritDoc
*/
public function addRules($rules)
{
$response = (new ValidationRuleParser($this->data))->explode($rules);
$this->rules = array_merge_recursive($this->rules, $response->rules);
$this->implicitAttributes = array_merge($this->implicitAttributes, $response->implicitAttributes);
}
/**
* Move the replacement of [$this->dotPlaceholder, '__asterisk__'] with ['.', '*'], as is the case in the
* parent::addFailure(). The asterisk won't be present anyway, due to $this->addRules(). And the dots need to
* remain as $this->dotPlaceholder so $this->shouldBeExcluded() can locate the corresponding value properly by
* $this->excludeAttribute(). But the conversion does still need to be done before the error message is produced.
* @inheritDoc
*/
public function addFailure($attribute, $rule, $parameters = [])
{
!$this->messages && $this->passes();
if (in_array($rule, $this->excludeRules)) {
$this->excludeAttribute($attribute);
return;
}
$attribute = str_replace($this->dotPlaceholder, '\.', $attribute);
$message = $this->getMessage($attribute, $rule);
$message = $this->makeReplacements($message, $attribute, $rule, $parameters);
$this->messages->add($attribute, $message);
$this->failedRules[$attribute][$rule] = $parameters;
}
/**
* If the rule is one that references another field, the first parameter/field must go through the same dot
* placeholder conversion as the field names in order for a match to be possible
* @inheritDoc
*/
protected function validateAttribute($attribute, $rule)
{
$this->currentRule = $rule;
[$rule, $parameters] = ValidationRuleParser::parse($rule);
if (in_array($rule, $this->excludeRules)) {
$parameters[0] = str_replace('\.', $this->dotPlaceholder, $parameters[0]);
}
if (!$rule) {
return null;
}
if (($keys = $this->getExplicitKeys($attribute)) && $this->dependsOnOtherFields($rule)) {
$parameters = $this->replaceAsterisksInParameters($parameters, $keys);
}
$value = $this->getValue($attribute);
$rules = array_merge($this->fileRules, $this->implicitRules);
if ($value instanceof UploadedFile && ! $value->isValid() && $this->hasRule($attribute, $rules)) {
$this->addFailure($attribute, 'uploaded', []);
return null;
}
$validatable = $this->isValidatable($rule, $attribute, $value);
if ($rule instanceof RuleContract) {
$validatable && $this->validateUsingCustomRule($attribute, $value, $rule);
return null;
}
$method = "validate{$rule}";
$isValid = $this->$method($attribute, $value, $parameters, $this);
if ($validatable && !$isValid) {
$this->addFailure($attribute, $rule, $parameters);
}
}
/**
* Ignore 'required' rules on elements within arrays that are missing in their entirety. The parent arrays can be
* set to 'required' if their existence is necessary, rather than expect it as a side effect of nested data.
* @inheritDoc
*/
public function validateRequired($attribute, $value)
{
if (!($valid = parent::validateRequired($attribute, $value))) {
$parent = explode('.', $attribute);
$attributeIsNested = count($parent) >= 2;
array_pop($parent);
$parent = implode('.', $parent);
$parentExists = $this->getValue($parent);
$valid |= ($attributeIsNested && !$parentExists);
}
return $valid;
}
/**
* ISO8601 can be represented with a colon in the timezone ("c"), without one (DateTime::ISO8601), or Zulu
* @inheritDoc
*/
public function validateDateFormat($attribute, $value, $parameters)
{
$this->requireParameterCount(1, $parameters, 'date_format');
if (!is_string($value) && !is_numeric($value)) {
return false;
}
$format = $parameters[0];
$date = DateTime::createFromFormat("!$format", $value);
$acceptable = [$date->format($format)];
if (in_array($format, [DateTime::ISO8601, 'c', 'Y-m-d\TH:i:s\Z'])) {
$acceptable += [
$date->format(DateTime::ISO8601),
$date->format('c'),
gmdate('Y-m-d\TH:i:s\Z', $date->getTimestamp())
];
}
return $date && in_array($value, $acceptable);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment