«Runn Me!» — говорит нам очередной фреймворк* на PHP. А слышится «Throw Me!». Часть 2

в 14:28, , рубрики: composer, framework, github, laravel, lgpl, library, php, PHP 7, symfony, yii, Zend Framework

* вообще говоря это пока еще не фреймворк, а просто набор библиотек, фреймворком он станет чуть позже

«Runn Me!» — говорит нам очередной фреймворк* на PHP. А слышится «Throw Me!». Часть 2 - 1

Не прошло и недели с момента «безумного успеха» (тут мнения немного расходятся, конечно...) первой части нашего повествования, как пришло время выпустить вторую.

Сегодня мы продолжаем путешествие в бездонную глубину библиотеки runn/core будущего фреймворка «Runn Me!». Под катом нам встретятся следующие обитатели бездны:

  • Концепция «мультиисключения» и ее реализация в библиотеке
  • Понятие объекта с внутренней валидацией и эталонная реализация такого объекта
  • Немного заглянем в мир валидаторов и санитайзеров (подробный рассказ о них будет позже)
  • Рассмотрим реализацию объекта с обязательными полями


«Runn Me!» — говорит нам очередной фреймворк* на PHP. А слышится «Throw Me!». Часть 2 - 2

Предыдущие серии:

  1. Историческая справка. ObjectAsArray. Collection. TypedCollection. Std class

Мультиисключение

В начале был интерфейс Throwable.

Хитрая штука. Напрямую реализовать нельзя, как и многие интерфейсы из стандартной библиотеки PHP, зато любой объект, реализующий этот интерфейс, получает особую суперспособность: его можно «выбросить» (throw). Впрочем, слово «любой» тут явное преувеличение: всё, что нам доступно — это унаследовать собственный класс от библиотечного же Exception.

Именно с такого наследования и начинается работа с исключениями в «Runn Me!»:

namespace RunnCore;

class Exception
    extends Exception
    implements JsonSerializable
{
    public function jsonSerialize()
    {
        return ['code' => $this->getCode(), 'message' => $this->getMessage()];
    }
}

Имплементация интерфейса JsonSerializable неспроста добавлена сразу же в RunnCoreException: это явный задел на будущее, на то светлое будущее, когда мы с вами научимся исключения ловить в наших middleware, упаковывать в JSON и отдавать в ответе клиенту.

Однако «Runn Me!» не так скучен, как может показаться при перечитывании листингов. Совсем рядом с классом Exception в пространстве имён RunnCore притаился еще один, почти незаметный класс RunnCoreExceptions. Что же это такое?

Это мультиисключение:

  • Типизированная коллекция (см. предыдущую статью)
  • Тип элементов этой коллекции — Throwable
  • И при этом она сама является Throwable!

Давайте посмотрим на несложном примере, как такой конструкцией можно пользоваться:

$exceptions = new Exceptions;

$exceptions->add(new Exception('First'));
$exceptions->add(new Exception('Second'));

assert(2 === count($exceptions));
assert('First'  === $exceptions[0]->getMessage());
assert('Second' === $exceptions[1]->getMessage());

if (!$exceptions->empty()) {
  throw $exceptions;
}

Где применяются мультиисключения?

Самое, пожалуй, «яркое» применение этого паттерна — механизм валидации, предусмотренный в стандартных объектах «Runn Me!».

Авторы библиотек прекрасно осознают, что даже намёк на то, что ошибка валидации может быть исключением, способен вызывать жжение пониже спины у многих программистов. Однако мы решили, что возможный профит от такого подхода во много раз превышает минусы, которые можно от него ожидать. Поэтому не пылайте зря — у нас ошибки валидации реализуются исключениями и мультиисключениями. Это решение принято и обжалованию не подлежит.

Рассмотрим подробно, шаг за шагом, как же это устроено.

Методы подключения валидации

Внутри стандартного объекта (а точнее — любого наследника HasInnerValidationInterface, если вы не слишком отступили от эталонной реализации, описанной в StdGetSetWValidateSanitizeTrait) вы можете определить методы подключения валидации.

Название такого метода должно начинаться со слова «validate», за которым следует имя ключа, значение для которого мы будем валидировать, с первой большой буквы. Например:

class ValidationExampleClass extends Std
{
  protected function validateFoo($val)
  {
    return true;
  }
  protected function validateBar($val)
  {
    return true;
  }  
}

$obj = new ValidationExampleClass;
$obj->foo = 42;
$obj->bar = 'baz';

В данном коде показано, что мы создали два метода для подключения валидации validateFoo() и validateBar(). Первый из них будет автоматически вызван ДО присваивания какого-либо значения свойству $obj->foo, второй же, соответственно, до фактического изменения $obj->baz.

Возможны пять вариантов «поведения» метода подключения валидации:

  1. Метод возвращает false
  2. Метод выбрасывает единичное исключение
  3. Метод выбрасывает мультиисключение
  4. Метод является генератором исключений
  5. И, наконец, метод просто вернул что-то, что не является false

Самые простые — это варианты №№ 1 и 5.

В первом случае просто ничего не происходит, присваивание «молча» отменяется, свойство не получает новое значение.

В пятом случае присваивание нового значения свойству также «молча» происходит, не вызывая никаких побочных эффектов.

Чуть сложнее варианты №№ 2, 3 и 4. Все они ведут к отмене присваивания. Но чтобы точно понять, для чего они предназначены, мы с вами пойдем дальше.

Методы массового присваивания (заполнения)

С целью наиболее полно использовать потенциал методов подключения валидации в трейте StdGetSetWValidateSanitizeTrait переопределен важный метод merge() (который, в том числе, используется и в конструкторе класса Std).

Показать его работу лучше всего на примере:

class ValidationExampleClass extends Std
{

  // Случай номер 2: единичное исключение
  protected function validateFoo($val)
  {
    if (empty($val)) {
      throw new Exception('foo is empty');
    }
  }

  // Случай номер 3: мультиисключение
  protected function validateBar($val)
  {
    $errors = new ValidationErrors; // этот класс, разумеется, наследуется от Exceptions
    if (strlen($val) < 6) {
      $errors[] = new Exception('bar is too short');
    }
    if (preg_match('~d~', $val)) {
      $errors[] = new Exception('bar contains digits');
    }
    if (!$errors->empty()) {
      throw $errors;
    }
  }  

  // Случай номер 4: генератор исключений
  protected function validateBaz($val)
  {
    if (strlen($val) > 6) {
      yield new Exception('baz is too long');
    }
    if (preg_match('~[a-z]~', $val)) {
      yield new Exception('baz contains letters');
    }
  }  
}

Теперь, когда мы с вами определили все возможные правила валидации всеми возможными способами, давайте попробуем создать объект, который намеренно нарушит все эти правила:

try {

  $obj = new ValidationExampleClass([
    'foo' => '', // нарушаем правило "непустой"
    'bar' => '123', // две ошибки - "слишком коротко" и "содержит цифры",
    'baz' => 'abcdefgh', // две ошибки - "слишком длинно" и "содержит буквы",
  ]);

} catch (Exceptions $errors) {
  foreach ($errors as $error) {
    // Прекрасно! Мы получили все пять ошибок валидации!
    echo $error->getMessage();
  }
}

Что же произошло?

Для начала метод merge() готовит пустую коллекцию класса Exceptions для будущих ошибок валидации. Затем для каждого ключа вызывается, если он существует, метод подключения валидации.

2. Метод подключения валидации выбросил одиночное исключение: оно добавляется в коллекцию.
3. Метод выбросил мультиисключение: оно объединяется с коллекцией.
4. Метод является генератором: всё, что он сгенерирует, являющееся Throwable, будет добавлено в коллекцию.

Если после всех этих операций коллекция ошибок валидации оказалась непустой — она выбрасывается. Дальше уже вам надлежит ее где-то поймать и решить, что делать со всеми этими ошибками.

Методы подключения санитайзинга

Ну, тут рассказ будет не таким захватывающим, как про валидацию. Всё просто:

class SanitizationExampleClass extends Std
{
  protected function sanitizePhone($val)
  { 
    return preg_replace('~D~', '', $val);
  }
}

$obj = new SanitizationExampleClass;
$obj->phone = '+7 (900) 123-45-67';
assert('79001234567' === $obj->phone);

Определили метод, он получает на вход то значение, которое вы намереваетесь присвоить свойству, то, что возвращает — будет действительно присвоено. Банально, но полезно.

Небольшой анонс

Разумеется, «магическими» методами в стандарных объектах тема валидации и санитации не исчерпывается. Одна из следующих статей будет целиком посвящена библиотеке runn/validation, которая сейчас готовится к публикации.

И, наконец, обещанные обязательные поля

Важнейшая вещь, скажу я вам. Особенно когда мы перейдем к теме комплексных (очень хочется произнести это слово с ударением на «е»: «комплЕксных») объектов. Но и без них можно всё понять:

class testClassWithRequired extends Std {
  protected static $required = ['foo', 'bar']; 
  // если вам не хватит гибкости, можете переопределить метод getRequiredKeys() по своему вкусу
}

try {
  $obj = new testClassWithRequired();
} catch (Exceptions $errors) {

  assert(2 == count($errors));
  assert('Required property "foo" is missing' === $errors[0]->getMessage());
  assert('Required property "bar" is missing' === $errors[1]->getMessage());
}

Как вы видите, здесь используется всё тот же уже знакомый механизм мультиисключения для оповещения нас о том, что некоторые обязательные поля оказались неустановленными в конструкторе объекта. Кстати, если там возникнут ошибки валидации — мы их тоже увидим! Всё в той же коллекции $errors.

На сегодня всё. Следите за следующими статьями!

P.S. Детального плана со сроками выхода фреймворка в целом у нас нет, как нет и желания успеть к какой-то очередной дате. Поэтому не спрашивайте «когда». По мере готовности отдельных библиотек будут выходить статьи о них.

P.P.S. С благодарностью приму сведения об ошибках или опечатках в личные сообщения.

Хороших выходных всем!

Автор: Альберт Степанцев

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js