fathomminds/php-rest-models

View on GitHub
src/Schema/SchemaValidator.php

Summary

Maintainability
A
3 hrs
Test Coverage
<?php
namespace Fathomminds\Rest\Schema;

use Fathomminds\Rest\Schema\TypeValidators\ValidatorFactory;
use Fathomminds\Rest\Exceptions\RestException;

class SchemaValidator
{
    protected $fields = [];
    protected $allowExtraneous = false;
    protected $requiredSchemaClass = null;
    private $updateMode = false;
    private $replaceMode = false;

    public function __construct($requiredSchemaClass = null)
    {
        $this->requiredSchemaClass = $requiredSchemaClass;
    }

    public function updateMode($updateMode = null)
    {
        if (is_bool($updateMode)) {
            $this->updateMode = $updateMode;
        }
        return $this->updateMode;
    }

    public function replaceMode($replaceMode = null)
    {
        if (is_bool($replaceMode)) {
            $this->replaceMode = $replaceMode;
        }
        return $this->replaceMode;
    }

    public function validate($resource)
    {
        $this->validateResourceType($resource);
        $this->validateCircularDependency(get_class($resource), $resource->schema());
        $extraneousCheck = [];
        if (!$this->allowExtraneous) {
            $extraneousCheck = $this->validateExtraneousFields($resource);
        }
        $errors = array_merge(
            $this->validateRequiredFields($resource),
            $extraneousCheck,
            $this->validateFieldTypes($resource)
        );
        if (!empty($errors)) {
            throw new RestException(
                'Invalid structure',
                [
                    'schema' => get_class($resource),
                    'errors' => $errors,
                ]
            );
        }
    }

    private function validateCircularDependency($schemaName, $schemaDefinition, $schemaChain = [])
    {
        $schemaChain[] = $schemaName;
        foreach ($schemaDefinition as $field => $fieldDetails)
        {
            if (!isset($fieldDetails['type']) || $fieldDetails['type'] !== 'schema') {
                continue;
            }
            $nestedSchemaName = $fieldDetails['validator']['class'];
            $nestedSchemaDefinition = ($nestedSchemaName::cast((object)[]))->schema();
            if (in_array($nestedSchemaName, $schemaChain)) {
                $schemaChain[] = $nestedSchemaName;
                throw new RestException(
                    'Circular dependency found in schema definition',
                    [
                        'schema' => array_shift($schemaChain),
                        'chain' => $schemaChain,
                    ]
                );
            }
            $this->validateCircularDependency(
                $nestedSchemaName,
                $nestedSchemaDefinition,
                $schemaChain
            );
        }
    }

    public function allowExtraneous($value)
    {
        $this->allowExtraneous = $value;
    }

    private function validateResourceType($resource)
    {
        $this->expectObject($resource);
        $this->objectHasSchemaMethod($resource);
        $this->objectIsValidSchemaClass($resource);
    }

    private function expectObject($resource)
    {
        if (gettype($resource) !== 'object') {
            throw new RestException(
                'Object expected',
                [
                    'schema' => static::class,
                    'type' => gettype($resource),
                ]
            );
        }
    }

    private function objectHasSchemaMethod($resource)
    {
        if (!method_exists($resource, 'schema')) {
            throw new RestException(
                'Object must be a correct RestSchema object',
                [
                    'schema' => static::class,
                    'type' => gettype($resource),
                ]
            );
        }
    }

    private function objectIsValidSchemaClass($resource)
    {
        if ($this->requiredSchemaClass === null) {
            return;
        }
        if (get_class($resource) !== $this->requiredSchemaClass) {
            throw new RestException(
                'Object must be an instance of the defined SchemaClass',
                [
                    'schema' => static::class,
                    'type' => gettype($resource),
                ]
            );
        }
    }

    private function validateRequiredFields($resource)
    {
        $errors = [];
        if ($this->updateMode()) {
            return $errors;
        }
        $missingFields = array_diff($this->getRequiredFields($resource), array_keys(get_object_vars($resource)));
        array_walk($missingFields, function($item) use (&$errors) {
            $errors[$item] = 'Missing required field';
        });
        return $errors;
    }

    private function validateExtraneousFields($resource)
    {
        $errors = [];
        $extraFields = array_diff(array_keys(get_object_vars($resource)), array_keys($resource->schema()));
        array_walk($extraFields, function($item) use (&$errors) {
            $errors[$item] = 'Extraneous field';
        });
        return $errors;
    }

    private function validateFieldTypes($resource)
    {
        $validatorFactory = new ValidatorFactory;
        $errors = [];
        foreach ($resource->schema() as $fieldName => $rules) {
            if (property_exists($resource, $fieldName)) {
                try {
                    $validatorFactory
                        ->create($rules, $this->updateMode(), $this->replaceMode())
                        ->validate($resource->{$fieldName});
                } catch (RestException $ex) {
                    $errors[$fieldName]['error'] = $ex->getMessage();
                    $errors[$fieldName]['details'] = $ex->getDetails();
                }
            }
        }
        return $errors;
    }

    private function filterFields($resource, $paramKey, $paramValue, $checkParamValue = true)
    {
        $fields = [];
        foreach ($resource->schema() as $fieldName => $params) {
            if (array_key_exists($paramKey, $params) && $this->isMatchedValue(
                    $checkParamValue,
                    $params[$paramKey],
                    $paramValue
                )) {
                $fields[$fieldName] = $params;
            }
        }
        return $fields;
    }

    private function isMatchedValue($checkRequired, $value, $valueToMatch)
    {
        if (!$checkRequired) {
            return true;
        }
        return ($value == $valueToMatch);
    }

    public function getFields($resource)
    {
        return $resource->schema();
    }

    public function getRequiredFields($resource)
    {
        return array_keys($this->filterFields($resource, 'required', true));
    }

    public function getUniqueFields($resource)
    {
        return array_merge(
            array_keys($this->filterFields($resource, 'unique', true)),
            $this->getNestedUniqueFieldNames($resource)
        );
    }

    public function getSchemaFieldsWithDetails($resource)
    {
        return $this->filterFields($resource, 'type', 'schema');
    }

    private function getNestedUniqueFieldNames($resource) {
        $result = [];
        $schemaFields = $this->getSchemaFieldsWithDetails($resource);
        array_walk($schemaFields, function($fieldDetails, $fieldName) use (&$result, &$resource) {
            $nestedResourceClass = $fieldDetails['validator']['class'];
            $nestedResource = property_exists($resource, $fieldName)
                ? $resource->{$fieldName}
                : $nestedResourceClass::cast((object)[]);
            $nestedUniqueFields = (new SchemaValidator($nestedResourceClass))->getUniqueFields($nestedResource);
            array_walk($nestedUniqueFields, function($nestedFieldName) use (&$result, &$fieldName) {
                $result[] = $fieldName . '.' . $nestedFieldName;
            });
        });
        return $result;
    }

    public function getFieldsWithDefaults($resource)
    {
        return $this->filterFields($resource, 'default', null, false);
    }
}