Skip to content
This repository has been archived by the owner on Jul 22, 2024. It is now read-only.

Commit

Permalink
Move unique_with validation rule to rinvex/laravel-support from corte…
Browse files Browse the repository at this point in the history
…x/foundation
  • Loading branch information
Omranic committed Jul 25, 2023
1 parent 8e54d2b commit 4736681
Show file tree
Hide file tree
Showing 4 changed files with 313 additions and 4 deletions.
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,26 @@ Install via `composer require rinvex/laravel-support`

## Usage

## Support Helpers

### `mimetypes()`
### `mimetypes()` helper

The `mimetypes` method gets valid mime types:
```php
$mimetypes = mimetypes();
```

### `timezones()`
### `timezones()` helper

The `timezones` method gets valid timezones:
```php
$timezones = timezones();
```

### unique_with Validator Rule

This feature contains a variant of the `validateUnique` rule for Laravel, that allows for validation of multi-column UNIQUE indexes.

It was forked and merged from the awesome [felixkiss/uniquewith-validator](https://github.com/felixkiss/uniquewith-validator) package, which at the time been outdated and un-maintained for a long time. Many thanks to core contributors for developing this.


## Changelog

Expand Down
8 changes: 8 additions & 0 deletions src/Providers/SupportServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
use Illuminate\Support\Collection;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Validator;
use Rinvex\Support\Validators\UniqueWithValidator;
use Illuminate\Support\Facades\Validator as ValidatorFacade;

class SupportServiceProvider extends ServiceProvider
{
Expand All @@ -28,5 +30,11 @@ public function boot()
Collection::macro('similar', function (Collection $newCollection) {
return $newCollection->diff($this)->isEmpty() && $this->diff($newCollection)->isEmpty();
});

// Add support for unique_with validator
ValidatorFacade::extend('unique_with', UniqueWithValidator::class.'@validateUniqueWith', trans('validation.unique_with'));
ValidatorFacade::replacer('unique_with', function () {
return call_user_func_array([new UniqueWithValidator(), 'replaceUniqueWith'], func_get_args());
});
}
}
249 changes: 249 additions & 0 deletions src/Validators/UniqueWithRuleParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
<?php

declare(strict_types=1);

namespace Rinvex\Support\Validators;

use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Model;

class UniqueWithRuleParser
{
protected $table;

protected $connection;

protected $primaryField;

protected $primaryValue;

protected $additionalFields;

protected $parsed = false;

protected $ignoreColumn;

protected $ignoreValue;

protected $dataFields;

protected $parameters;

protected $attribute;

protected $data;

public function __construct($attribute = null, $value = null, array $parameters = [], array $data = [])
{
$this->primaryField = $this->attribute = $attribute;
$this->primaryValue = $value;
$this->parameters = $parameters;
$this->data = $data;
}

protected function parse()
{
if ($this->parsed) {
return;
}
$this->parsed = true;

// cleaning: trim whitespace
$this->parameters = array_map('trim', $this->parameters);

// first item equals table name
$this->table = array_shift($this->parameters);
if (Str::contains($this->table, '.')) {
[$this->connection, $this->table] = explode('.', $this->table, 2);
}

// Check if ignore data is set
$this->parseIgnore();

// Parse field data
$this->parseFieldData();
}

protected function parseFieldData()
{
$this->additionalFields = [];
$this->dataFields = [$this->primaryField];

// Figure out whether field_name is the same as column_name
// or column_name is explicitly specified.
//
// case 1:
// $parameter = 'last_name'
// => field_name = column_name = 'last_name'
// case 2:
// $parameter = 'last_name=sur_name'
// => field_name = 'last_name', column_name = 'sur_name'
foreach ($this->parameters as $parameter) {
$parts = array_map('trim', explode('=', $parameter, 2));
$fieldName = $this->parseFieldName($parts[0]);
$columnName = count($parts) > 1 ? $parts[1] : $fieldName;
$this->dataFields[] = $fieldName;

if ($fieldName === $this->primaryField) {
$this->primaryField = $columnName;
continue;
}

if (! Arr::has($this->data, $fieldName)) {
continue;
}

$this->additionalFields[$columnName] = Arr::get($this->data, $fieldName);
}

$this->dataFields = array_values(array_unique($this->dataFields));
}

public function getConnection()
{
$this->parse();

return $this->connection;
}

public function getTable()
{
$this->parse();
$table = $this->table;

if (str_contains($table, '\\') && class_exists($table) && is_a($table, Model::class, true)) {
$model = new $table();
$table = $model->getTable();

return $model ? ($this->isValidationScoped($model) ? $model : $model->withoutGlobalScopes()) : (new AbstractModel())->setTable($table);
}

return $table;
}

/**
* Returns whether the model validation be scoped or not. (Default: true).
*
* @param \Illuminate\Database\Eloquent\Model $model
*
* @return bool
*/
protected function isValidationScoped(Model $model): bool
{
return $model->isValidationScoped ?? true;
}

public function getPrimaryField()
{
$this->parse();

return $this->primaryField;
}

public function getPrimaryValue()
{
$this->parse();

return $this->primaryValue;
}

public function getAdditionalFields()
{
$this->parse();

return $this->additionalFields;
}

public function getIgnoreValue()
{
$this->parse();

return $this->ignoreValue;
}

public function getIgnoreColumn()
{
$this->parse();

return $this->ignoreColumn;
}

public function getDataFields()
{
$this->parse();

return $this->dataFields;
}

protected function parseIgnore()
{
// Ignore has to be specified as the last parameter
$lastParameter = end($this->parameters);
if (! $this->isIgnore($lastParameter)) {
return;
}

$lastParameter = array_map('trim', explode('=', $lastParameter));

$this->ignoreValue = str_replace('ignore:', '', $lastParameter[0]);
$this->ignoreColumn = (count($lastParameter) > 1) ? end($lastParameter) : null;

// Shave of the ignore_id from the array for later processing
array_pop($this->parameters);
}

protected function isIgnore($parameter)
{
// An ignore_id can be specified by prefixing with 'ignore:'
if (mb_strpos($parameter, 'ignore:') !== false) {
return true;
}

// An ignore_id can be specified if parameter starts with a
// number greater than 1 (a valid id in the database)
$parts = array_map('trim', explode('=', $parameter));

return preg_match('/^[1-9][0-9]*$/', $parts[0]);
}

protected function parseFieldName($field)
{
if (preg_match('/^\*\.|\.\*\./', $field)) {
// This rule validates multiple times, because a wildcard * was used
// in order to validate all elements of an array. We now need to
// figure out which element we are on, so we can replace the
// wildcard with the current index in the array to access the actual
// data correctly.

// 1. Convert main attribute (Laravel has already replaced the
// wildcards with the current indizes here) to have wildcards
// instead
$attributeWithWildcards = preg_replace(
['/^[0-9]+\./', '/\.[0-9]+\./'],
['*.', '.*.'],
$this->attribute
);

// 2. Figure out what parts of the current field string should be
// replaced (Basically everything before the last wildcard)
$positionOfLastWildcard = mb_strrpos($attributeWithWildcards, '*.');
$wildcardPartToBeReplaced = mb_substr($attributeWithWildcards, 0, $positionOfLastWildcard + 2);

// 3. Figure out what the substitute for the replacement in the
// current field string should be (Basically delete everything
// after the final index part in the main attribute)
$endPartToDismiss = mb_substr($attributeWithWildcards, $positionOfLastWildcard + 2);
$actualIndexPartToBeSubstitute = str_replace($endPartToDismiss, '', $this->attribute);

// 4. Do the actual replacement. The end result should be a string
// of the current field we work on, but with the wildcards
// replaced by the correct indizes for the current validation run
$fieldWithActualIndizes = str_replace($wildcardPartToBeReplaced, $actualIndexPartToBeSubstitute, $field);

return $fieldWithActualIndizes;
}

return $field;
}
}
48 changes: 48 additions & 0 deletions src/Validators/UniqueWithValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

namespace Rinvex\Support\Validators;

use Illuminate\Support\Arr;
use Illuminate\Support\Str;

class UniqueWithValidator
{
public function validateUniqueWith($attribute, $value, $parameters, $validator)
{
$ruleParser = new UniqueWithRuleParser($attribute, $value, $parameters, $validator->getData());

// The presence verifier is responsible for counting rows within this
// store mechanism which might be a relational database or any other
// permanent data store like Redis, etc. We will use it to determine
// uniqueness.
$presenceVerifier = $validator->getPresenceVerifier();
if (method_exists($presenceVerifier, 'setConnection')) {
$presenceVerifier->setConnection($ruleParser->getConnection());
}

return $presenceVerifier->getCount($ruleParser->getTable(), $ruleParser->getPrimaryField(), $ruleParser->getPrimaryValue(), $ruleParser->getIgnoreValue(), $ruleParser->getIgnoreColumn(), $ruleParser->getAdditionalFields()) === 0;
}

public function replaceUniqueWith($message, $attribute, $rule, $parameters, $validator)
{
$translator = $validator->getTranslator();

$ruleParser = new UniqueWithRuleParser($attribute, null, $parameters);
$fields = $ruleParser->getDataFields();

if (method_exists($translator, 'trans')) {
$customAttributes = $translator->trans('validation.attributes');
} else {
$customAttributes = $translator->get('validation.attributes');
}

// Check if translator has custom validation attributes for the fields
$fields = array_map(function ($field) use ($customAttributes) {
return Arr::get($customAttributes, $field) ?: str_replace('_', ' ', Str::snake($field));
}, $fields);

return str_replace(':fields', implode(', ', $fields), $message);
}
}

0 comments on commit 4736681

Please sign in to comment.