Bladeren bron

added validators and global middlewares

Плотников Роман Вячеславович 3 jaren geleden
bovenliggende
commit
61aa5f3d8c

+ 37 - 16
app/Controller/Site.php

@@ -12,6 +12,8 @@ use Model\State;
 use Model\Division;
 use Model\Company;
 use Illuminate\Database\QueryException;
+use Src\Validator\Validator;
+
 
 class Site
 {
@@ -34,18 +36,36 @@ class Site
             $post = $request->post;
             $states = State::all();
 
+            $validator = new Validator($request->all(), [
+                'first_name' => ['required'],
+                'login' => ['required', 'unique:users,login'],
+                'password' => ['required'],
+                'last_name' => ['required'],
+                'middle_name' => ['required'],
+                'post' => ['required'],
+                'birth_date' => ['required'],
+                'home_address' => ['required']
+            ], [
+                'required' => 'Поле :field пусто',
+                'unique' => 'Поле :field должно быть уникально'
+            ]);
+
+            if ($validator->fails()) {
+                return new View(
+                    'site.signup',
+                    ['error' => json_encode($validator->errors(), JSON_UNESCAPED_UNICODE)]
+                );
+            }
+
+
             if ($post['password'] !== $post['password2']) $error = $error . 'Пароли не совпадают <br>';
             if ($post['state'] === 'fake') $error = $error . 'Выберите штат <br>';
             if ($post['gender'] === 'fake') $error = $error . 'Выберите пол <br>';
 
             if (!empty($error)) return new View('site.signup', ['error' => $error, 'states' => $states]);
 
-            try {
-                if (User::create($request->all())) {
-                    app()->route->redirect('/hello');
-                }
-            } catch (QueryException $e) {
-                return new View('site.signup', ['error' => 'Логин занят.', 'states' => $states]);
+            if (User::create($request->all())) {
+                app()->route->redirect('/hello');
             }
         }
 
@@ -157,13 +177,14 @@ class Site
         }
     }
 
-    public function createNewState(Request $request): string {
+    public function createNewState(Request $request): string
+    {
         $divisions = Division::all();
         if ($request->method === "GET") return (new View)->render('site.create_new_state', ['divisions' => $divisions]);
-        
+
         $post = $request->post;
         $error = '';
-        
+
         $divisionId = $post['division'];
         $allDivisionStates = State::where('division', $divisionId)->get();
 
@@ -177,13 +198,13 @@ class Site
                 break;
             }
         }
-        
+
         if (!empty($error)) return new View('site.create_new_state', ['error' => $error, 'divisions' => $divisions]);
 
         if (State::create($request->all())) {
             return (new View)->render('site.create_new_state', ['error' => 'Штат успешно создан']);
         }
-        
+
         return (new View)->render('site.create_new_state', ['error' => 'Произошла ошибка']);
     }
 
@@ -197,7 +218,7 @@ class Site
         $post = $request->post;
 
         if ($post['company'] === 'fake') $error = $error . 'Выберите компанию <br>';
-        
+
         if (!empty($error)) return $view->render('site.create_new_division', ['companies' => $companies, 'error' => $error]);
 
         $divisionsWithSameName = Division::where(['name' => $post['name'], 'company' => $post['company']])->get();
@@ -207,7 +228,7 @@ class Site
                 return $view->render('site.create_new_division', ['companies' => $companies, 'error' => $error]);
             }
         }
-        
+
         if (Division::create($request->all())) {
             return $view->render('site.create_new_division', ['companies' => $companies, 'error' => 'Успешно создано']);
         } else {
@@ -232,7 +253,7 @@ class Site
 
         $post = $request->post;
 
-        if($post['id'] === 'fake') return $view->render('site.delete_user', ['users' => $users, 'message' => 'Выберите пользователя']);
+        if ($post['id'] === 'fake') return $view->render('site.delete_user', ['users' => $users, 'message' => 'Выберите пользователя']);
 
         $user = User::where('id', $post['id'])->first();
 
@@ -256,7 +277,7 @@ class Site
 
         $post = $request->post;
 
-        if($post['id'] === 'fake') return $view->render('site.delete_state', ['states' => $states, 'message' => 'Выберите штат']);
+        if ($post['id'] === 'fake') return $view->render('site.delete_state', ['states' => $states, 'message' => 'Выберите штат']);
 
         $state = State::where('id', $post['id'])->first();
 
@@ -274,7 +295,7 @@ class Site
 
         $post = $request->post;
 
-        if($post['id'] === 'fake') return $view->render('site.delete_division', ['divisions' => $divisions, 'message' => 'Выберите подразделение']);
+        if ($post['id'] === 'fake') return $view->render('site.delete_division', ['divisions' => $divisions, 'message' => 'Выберите подразделение']);
 
         $division = Division::where('id', $post['id'])->first();
 

+ 16 - 0
app/Middlewares/SpecialCharsMiddleware.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace Middlewares;
+
+use Src\Request;
+
+class SpecialCharsMiddleware
+{
+    public function handle(Request $request): Request
+    {
+        foreach ($request->all() as $key => $value) {
+            $request->set($key, is_string($value) ? htmlspecialchars($value) : $value);
+        }
+        return $request;
+    }
+}

+ 16 - 0
app/Middlewares/TrimMiddleware.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace Middlewares;
+
+use Src\Request;
+
+class TrimMiddleware
+{
+   public function handle(Request $request)
+   {
+       foreach ($request->all() as $key => $value) {
+           $request->set($key, is_string($value) ? trim($value) : $value);
+       }
+       return $request;
+   }
+}

+ 16 - 0
app/Validators/RequireValidator.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace Validators;
+
+use Src\Validator\AbstractValidator;
+
+class RequireValidator extends AbstractValidator
+{
+
+   protected string $message = 'Field :field is required';
+
+   public function rule(): bool
+   {
+       return !empty($this->value);
+   }
+}

+ 18 - 0
app/Validators/UniqueValidator.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace Validators;
+
+use Illuminate\Database\Capsule\Manager as Capsule;
+use Src\Validator\AbstractValidator;
+
+class UniqueValidator extends AbstractValidator
+{
+
+    protected string $message = 'Field :field must be unique';
+
+    public function rule(): bool
+    {
+        return (bool)!Capsule::table($this->args[0])
+            ->where($this->args[1], $this->value)->count();
+    }
+}

+ 17 - 9
config/app.php

@@ -1,12 +1,20 @@
 <?php
 return [
-   //Класс аутентификации
-   'auth' => \Src\Auth\Auth::class,
-   //Клас пользователя
-   'identity' => \Model\User::class,
-   //Классы для middleware
-   'routeMiddleware' => [
-       'auth' => \Middlewares\AuthMiddleware::class,
-       'isadmin' => \Middlewares\IsAdminMiddleware::class,
-   ]
+    //Класс аутентификации
+    'auth' => \Src\Auth\Auth::class,
+    //Клас пользователя
+    'identity' => \Model\User::class,
+    //Классы для middleware
+    'routeMiddleware' => [
+        'auth' => \Middlewares\AuthMiddleware::class,
+        'isadmin' => \Middlewares\IsAdminMiddleware::class,
+    ],
+    'routeAppMiddleware' => [
+        'trim' => \Middlewares\TrimMiddleware::class,
+        'specialChars' => \Middlewares\SpecialCharsMiddleware::class,
+    ],
+    'validators' => [
+        'required' => \Validators\RequireValidator::class,
+        'unique' => \Validators\UniqueValidator::class
+    ]
 ];

+ 26 - 7
core/Src/Middleware.php

@@ -30,10 +30,21 @@ class Middleware
         $this->middlewareCollector = new RouteCollector(new Std(), new MarkBased());
     }
 
+    //Поиск middlewares по адресу
+    private function getMiddlewaresForRoute(string $httpMethod, string $uri): array
+    {
+        $dispatcherMiddleware = new Dispatcher($this->middlewareCollector->getData());
+        return $dispatcherMiddleware->dispatch($httpMethod, $uri)[1] ?? [];
+    }
+    //Запуск всех middlewares
+    public function go(string $httpMethod, string $uri, Request $request): Request
+    {
+        return $this->runMiddlewares($httpMethod, $uri, $this->runAppMiddlewares($request));
+    }
+
     //Запуск всех middlewares для текущего маршрута
-    public function runMiddlewares(string $httpMethod, string $uri): Request
+    private function runMiddlewares(string $httpMethod, string $uri, Request $request): Request
     {
-        $request = new Request();
         //Получаем список всех разрешенных классов middlewares из настроек приложения
         $routeMiddleware = app()->settings->app['routeMiddleware'];
 
@@ -41,16 +52,24 @@ class Middleware
         foreach ($this->getMiddlewaresForRoute($httpMethod, $uri) as $middleware) {
             $args = explode(':', $middleware);
             //Создаем объект и вызываем метод handle
-            (new $routeMiddleware[$args[0]])->handle($request, $args[1] ?? null);
+            $request = (new $routeMiddleware[$args[0]])->handle($request, $args[1] ?? null) ?? $request;
         }
         //Возвращаем итоговый request
         return $request;
     }
 
-    //Поиск middlewares по адресу
-    private function getMiddlewaresForRoute(string $httpMethod, string $uri): array
+    //Запуск всех глобальных middlewares
+    private function runAppMiddlewares(Request $request): Request
     {
-        $dispatcherMiddleware = new Dispatcher($this->middlewareCollector->getData());
-        return $dispatcherMiddleware->dispatch($httpMethod, $uri)[1] ?? [];
+        //Получаем список всех разрешенных классов middlewares из настроек приложения
+        $routeMiddleware = app()->settings->app['routeAppMiddleware'];
+
+        //Перебираем и запускаем их
+        foreach ($routeMiddleware as $name => $class) {
+            $args = explode(':', $name);
+
+            $request = (new $class)->handle($request, $args[1] ?? null) ?? $request;
+        }
+        return $request;
     }
 }

+ 2 - 2
core/Src/Route.php

@@ -73,7 +73,6 @@ class Route
     {
         Settings::addUri($this->prefix . '.' . $this->currentRoute);
     }
-    
     public function start(): void
     {
         // Fetch method and URI from somewhere
@@ -98,7 +97,8 @@ class Route
             case Dispatcher::FOUND:
                 $handler = $routeInfo[1];
                 $vars = array_values($routeInfo[2]);
-                $vars[] = Middleware::single()->runMiddlewares($httpMethod, $uri);
+                //Вызываем обработку всех Middleware
+                $vars[] = Middleware::single()->go($httpMethod, $uri, new Request());
                 $class = $handler[0];
                 $action = $handler[1];
                 call_user_func([new $class, $action], ...$vars);

+ 50 - 0
core/Src/Validator/AbstractValidator.php

@@ -0,0 +1,50 @@
+<?php
+
+namespace Src\Validator;
+
+abstract class AbstractValidator
+{
+    //Наименование валидируемого поля
+    protected string $field = '';
+    //Значение валидируемого поля
+    protected $value;
+    //Дополнительные аргументы
+    protected array $args = [];
+    //Массив ключей для замены в строке с ошибкой
+    protected array $messageKeys = [];
+    //Базовое сообщение об ошибке
+    protected string $message = '';
+
+    public function __construct(string $fieldName, $value, $args = [], string $message = null)
+    {
+        $this->field = $fieldName;
+        $this->value = $value;
+        $this->args = $args;
+        $this->message = $message ?? $this->message;
+
+        $this->messageKeys = [
+            ":value" => $this->value,
+            ":field" => $this->field
+        ];
+    }
+
+    //Если правило валидации не прошло, то возвращаем сообщение об ошибке
+    public function validate()
+    {
+        if (!$this->rule())
+            return $this->messageError();
+        return true;
+    }
+
+    //Замена ключей на конкретные значения в сообщении об ошибке
+    private function messageError(): string
+    {
+        foreach ($this->messageKeys as $key => $value) {
+            $message = str_replace($key, (string)$value, $this->message);
+        }
+        return $message;
+    }
+
+    //Основное правило валидации. Его должны переопределить классы-потомки
+    abstract public function rule(): bool;
+}

+ 77 - 0
core/Src/Validator/Validator.php

@@ -0,0 +1,77 @@
+<?php
+
+namespace Src\Validator;
+
+class Validator
+{
+    //Разрешенные валидаторы
+    private array $validators = [];
+    //Итоговые ошибки
+    private array $errors = [];
+    //Проверяемые поля
+    private array $fields = [];
+    //Массив правил
+    private array $rules = [];
+    //Кастомные сообщения
+    private array $messages = [];
+
+    public function __construct(array $fields, array $rules, array $messages = [])
+    {
+        $this->validators = app()->settings->app['validators'] ?? [];
+        $this->fields = $fields;
+        $this->rules = $rules;
+        $this->messages = $messages;
+        $this->validate();
+    }
+
+    //Перебираем список всех валидируемых полей и для
+    //каждого поля вызываем метод validateField()
+    private function validate(): void
+    {
+        foreach ($this->rules as $fieldName => $fieldValidators) {
+            $this->validateField($fieldName, $fieldValidators);
+        }
+    }
+
+    //Валидация отдельного поля
+    private function validateField(string $fieldName, array $fieldValidators): void
+    {
+        //Перебираем все валидаторы, ассоциированные с полем
+        foreach ($fieldValidators as $validatorName) {
+            //Отделяем от имени валидатора дополнительные аргументы
+            $tmp = explode(':', $validatorName);
+            [$validatorName, $args] = count($tmp) > 1 ? $tmp : [$validatorName, null];
+            $args = isset($args) ? explode(',', $args) : [];
+
+            //Соотносим имя валидатора с классом в массиве разрешенных валидаторов
+            $validatorClass = $this->validators[$validatorName];
+            if (!class_exists($validatorClass)) {
+                continue;
+            }
+            //Создаем объект валидатора, передаем туда параметры
+            $validator = new $validatorClass(
+                $fieldName,
+                $this->fields[$fieldName],
+                $args,
+                $this->messages[$validatorName]
+            );
+
+            //Если валидация не прошла, то добавляем ошибку в общий массив ошибок
+            if (!$validator->rule()) {
+                $this->errors[$fieldName][] = $validator->validate();
+            }
+        }
+    }
+
+    //Возврат массива найденных ошибок
+    public function errors(): array
+    {
+        return $this->errors;
+    }
+
+    //Признак успешной валидации
+    public function fails(): bool
+    {
+        return (bool)count($this->errors);
+    }
+}