rinvex/laravel-support

View on GitHub
src/Validators/UniqueWithRuleParser.php

Summary

Maintainability
A
35 mins
Test Coverage
<?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;
    }
}