Костылей всегда можно наплодить. Но в любом случае Action=Validate делать нельзя. Нельзя отправлять семантически разные запросы на один адрес. Нужно создавать дополнительные адреса на подобии таких:
`
POST /user/isValidEmail
POST /user/isValidUsername
`
И да. Наличие такого метода это серьёзная дыра в безопастности позволяющая собрать базу email ваших пользователей. На месте бизнеса я бы ещё подумал что хуже — уменьшение конверсии или утечка персональных данных пользователей.
Валедировать чернз API не входит в принцыпы REST. Я считаю что без этого можно обойтись. Это как в Web. Есть валидация на стороне клиента и на стороне сервера. На клиенте мы проверяем заполненность полей и формат данных, а наличие совпадений в бд уже после отпрвки формы на сервере.
Да. Проверить наличие совпадений до отправки может быть удобно, но без этого можно обойтись
1. Возможность централизованно мигрировать различные инстансы
миграции применяются при деплое автоматически или вручную. Сначала на тестовом сервере с использованием CI, а потом на боевом.
2. Уметь определять ошибки миграции и править их в полуавтоматическом режиме
запихнул миграцию в транзакцию и если она не выполнилась, транзакцию откатываем. Это делается через систему мигрирования.
Править миграцию в автоматическом режиме невозможно и не нужно.
3. Иметь систему прав доступа и аппрува изменений
Проект имеет доступ к всей базе и менять может всю базу. На уровне миграции разделения прав доступа быть не может. Это задача другого порядка.
4. Желательно уметь связывать миграции с версией кода
миграции нужно хранить в репозитории с кодом
5. Желательно уметь автоматически генерировать миграции на основании уже внесённых изменений
Многие системы миграций умеют генерить код на основе БД и миграции на основе кода. Doctrine Migrations по крайней мере точно умеет
6. Комментировать и привязывать к задачам каждую миграцию
добавляя к коммиту с миграцией номер задачи мы автоматически связываем их. Так работает GitHub и GitLab
7. Сравнивать итоговый DDL произвольных моментов в жизненном цикле.
Для того что бы увидеть конечный результат нужно применить миграции.
Обычно деплой мастер применяет все миграции на локальной базе и проверяет их на конфликты и потом при желании собирает их в одну большую миграцию.
видел реализацию в Yii
Я лично считаю не правильным использовать PHP конструкции для описания миграции. Во всех проектах которые я видел миграции описывались как SQL. Где-то это был *.sql файл, где-то PHP класс в котором описывались изменения как SQL. Для Yii можно вызвать execute(), для Doctrine Migrations addSql()
Получать у этих людей SQL миграции и отдавать разработчикам
Если миграции пишет разработчик БД, а не разработчик приложения, он все равно должен сохранить миграцию в проекте. Для упрощения можно описывать миграции, как говорил AlexLeonov, в отдельный *.sql файлах. Любой нормальный редактор будет поддерживать подсветку SQL синтаксиса в таких файлах.
+1 автор ищет проблемы на своё мягкое место. За изменение БД должен отвечать программист который делал это изменение и миграции должны хранится вместе с кодом проекта.
Я в PHP использую DoctrineMigrations
Я, как и другие пользователи хабра, не знаем ничего. Абсолютно ничего о вашем проекте и о проблемах с которыми вы сталкиваетесь.
Подведем итоги:
1. Вам нужно было в статья привести пример проблемы который вы озвучили в комментариях, тогда бы ваша статья была бы понятней и не вызвала бы такую бурю негативных эмоций. Ваше решение, в отрыве от решаемой им проблемы, выглядит бессмысленно и бесполезно.
2. Кроме вашего DataObject есть и другие методы решения описанной проблемы, но в целом оно вполне имеет право на жизнь.
Спасибо за интересную дискуссию. На этой замечательной ноте предлагаю закруглится.
Из вас всю информацию приходится прям клещами вытягивать.
Получается так. Есть базовый, не изменяемый функционал. И создаются новые плагины которые расширяют базовый функционал не затрагивая базовый класс. Базовый класс изменять условно нельзя.
В таком случае нужно сразу сказать что дополнительные поля из плагинов не должны быть обязательными для заполнения, иначе они могут поломать базовый функционал.
Я вижу 2 пути решения проблемы. Вариант с хранением в БД JSON я не рассматриваю потому что это… извращение. В таком случает лучше сразу хранить все данные в документоориентированных СУБД.
1. Создание отдельной таблицы с дополнительными полями необходимыми для плагина и сделать связь с базовой таблицей OneToOne.
Преимущества:
Ни базовый класс, ни базовая таблица никак не затрагиваются. Новая сущность существует параллельно с базовой. Доступ осуществляется так:
Недостатки:
Нет возможности получить сущность плагина из базовой сущности. Так как мы не можем изменять базовый класс, следующий вариант работать не будет:
$customer->getCustomerRef()->getRef();
Нужно создавать еще одну таблицу и делать JOIN для выбора дополнительных данных
2. Расширить функционал базового класса через extends и новые поля из класса CustomerRef должны просто игнорироваться движком.
Преимущества:
Просто в реализации. Код будет выглядеть так:
$customer->getId();
// $customer->getRef(); // no work
$customer_ref->getId();
$customer_ref->getRef();
Недостатки:
Опять же нельзя получить поля плагина из базового класса.
Усложняется разработка движка который будет отвечать за загрузку/сохранение данных. При неправильной реализации могут потеряться значения дополнительных полей CustomerRef при сохранении объекта как Customer.
Итог
Оба варианты рабочие, но не применимы для тех случаев когда нужно получить дополнительные поля из оригинального объекта. Это не страшно в тех случаях если плагины используются только для создания новой функциональности и фатально для тех случаев когда нужно переопределить базовую функциональность.
Ваше решение с DataObject позволяет получить дополнительные поля из базового класса, хотя автодополнение в IDE работать не будет.
PS: надо отметь что отсутствие возможности получить поля плагина из базового класса не всегда является минусом. В некоторых случаях это позволяет избежать ошибок.
Разработчик базового функционала заложил только $id. Атрибут $ref заложил разработчик плагина 1, атрибут $email — разработчик плагина 2. Разработчики друг с другом не знакомы.
Я не могу вам предложить конкретное решение вашей проблемы потому что не знаю особенностей архитектуры вашего приложение и не знаю на что вы можете влиять. Судя по тому что вы предлагаете в статье, вы можете влиять на код всех 3-х компонентов: «базовый функционал», «плагина 1» и «плагина 2».
Самое простое решение, если вы не можете влиять на код плагинов, это добавить в базовый функционал атрибут $ref из плагина 1 и атрибут $email из плагина 2. Базовый класс будет описывать реальную сущность из БД, а сущности из плагинов будут ориентироваться на свой интерфейсом как и раньше, а дополнительные атрибуты из другого плагина будут просто игнорировать.
Вот. С этого и стоило начинать. Это уже нормальное описание проблемы. Его и нужно было приводить в статье. Тогда и вопросов было бы меньше.
Пример решения с использованием декораторов и включенным strict mode:
Структура классов
declare(strict_types=1);
interface CustomerInterface
{
public function getId(): int;
public function setId(int $id);
}
interface CustomerPluginInterface extends CustomerInterface
{
public function getCustomer();
}
// This is base object.
class Customer implements CustomerInterface
{
private $id;
private $ref;
private $email;
public function getId(): int
{
return $this->id;
}
public function setId(int $id)
{
$this->id = $id;
return $this;
}
public function getRef(): string
{
return $this->ref;
}
public function setRef(string $ref)
{
$this->ref = $ref;
return $this;
}
public function getEmail(): string
{
return $this->email;
}
public function setEmail(string $email)
{
$this->email = $email;
return $this;
}
}
// This is extended customer (plugin 1).
class CustomerRef implements CustomerPluginInterface
{
private $customer;
public function __construct(Customer $customer)
{
$this->customer = $customer;
}
public function getId(): int
{
return $this->customer->getId();
}
public function setId(int $id)
{
$this->customer->setId($id);
return $this;
}
public function getRef(): string
{
return $this->customer->getRef();
}
public function setRef(string $ref)
{
$this->customer->setRef($ref);
return $this;
}
public function getCustomer()
{
return $this->customer;
}
}
// This is extended customer (plugin 2).
class CustomerEmail implements CustomerPluginInterface
{
private $customer;
public function __construct(Customer $customer)
{
$this->customer = $customer;
}
public function getId(): int
{
return $this->customer->getId();
}
public function setId(int $id)
{
$this->customer->setId($id);
return $this;
}
public function getEmail(): string
{
return $this->customer->getEmail();
}
public function setEmail(string $email)
{
$this->customer->setEmail($email);
return $this;
}
public function getCustomer()
{
return $this->customer;
}
}
под рукой нет php7 чтобы проверить, но должно работать
исполняемый код полностью копирует ваш:
$base = $repo->load('Customer', 21);
// plugin1 code on event 1
$cust1 = new CustomerRef($base);
$id1 = $cust1->getId();
$ref = $cust1->getRef();
// plugin2 code on event 2
$cust2 = new CustomerEmail($base);
$id2 = $cust2->getId();
$cust2->setEmail('any@email.com');
// data saver (base impl.)
$repo->save('Customer', $cust2->getCustomer()); // <-- различие только здесь
// но можно и так, ибо изменив $cust2 мы изменили $base
//$repo->save('Customer', $base);
С использованием DataObject нельзя контролировать тип хранящихся в нем данных и список самих данных. Например что будет при выполнении следующего кода? Вы будете в методе save() проверять список доступных полей на запись или будете писать данные как есть в БД и получать fatal error из-за отсутствия соответствующей колонки в таблице?
Ну вот! Так это то, о чем я и писал!!! Делая класс на базе DataObject вы делаете его:
а) типизируемым;
б) дополняемым;
Для этого создается обычный класс. DataObject здесь не нужен. Он не решает никаких проблем, а только создает их
Проблема в том, что в некоторых случаях множественное наследование не работает. Например, когда два-три расширения переопределяют один и тот же класс основного функционала.
Множественное наследование и не должно работать. В вашем случае нужно использовать Адаптор. Использование DataObject в описанном вами случае больше похоже на костыль
Так я и думал. Модель это не только данные, но еще и бизнес логика. Бизнес логика должна находится как можно ближе к данным. Именно по этому (еще из-за производительности) некоторые размещают бизнес логику в тригерах и процедурах БД, рядом с хранимыми данными. И именно по этому класс модели должен быть типизирован и хранить данные и бизнес логику одной конкретной сущности. Смотрите тот же ActiveRecord.
Если же нужен контейнер для временного хранения данных, то, как и говорили michael_vostrikov и lair, достаточно обычного массива
Когда вы говорите «мухи», «котлеты», у меня создается ощущение что вы не слышали про MVC.
По поводу вашего DataObject. Я подобное создавал в первый год изучения прогркммирования и очень бытсро отказался от этого решения. Основная проблема это отсутствие типизации и жесткой структуры объектов и с автодополнением в те годы были проблемы.
У меня вопрос. Правильно ли я понимаю что при использовании HTTPS не будет работать HTTP кэширование? Это вполне ожидаемо в случае HTTPS, но вызывает дополнительную нагрузку на сервер и канал. Мало того что придется шифровать запрос/ответ, но еще и контент придется отдавать на каждый запрос. Всю статику придется тянуть каждый раз.
Получается мы повышаем безопастность сайта в ущерб производительности. По моему это не совсем правильно. Поправьте меня если я ошибаюсь.
Этот вопрос разбирался в статье: «Согласно TDD, тесты предназначены для проверки функционала (feature), а не путей исполнения программы.». Не считайте пути исполнения программы (или ветки развития, как вы их называете), считайте сколько тестов вам нужно для тестирования конкретного функционала.
Хорошо. Тогда объясните что вы имеете в виду под словом функционал (feature)? Тестирование только особенностей тестируемого метода, без привязки к зависимостям? Если да, то тестирование получается не полным как и в случае с модульным. И мы опять возвращаемся к проблеме с Петей и John, ибо неполный тест может не отлавливать ситуации при которых Петя == John.
Что-то я ничего не понял. По-вашему, тесты к тестированию не имеют отношения? Что вы вообще хотите спросить\сказать?
Если что, речь шла про тесты в рамках TDD.
я говорил вот об этом
Доказательство:
Модульное тестирование, в отличие от интеграционного, вынуждает программистов инжектировать зависимости через конструктор или свойства. А если использовать интеграционное тестирование вместо модульного, то джуниор может зависимости прямо в коде класса инстанцировать.
Если повсеместно использовать DI то этой проблемы не будет и не будет разницы какие тесты вы пишете, интеграционные или модульные. Это организация процесса разработки и архитектуры приложения, ну и обучения джунов. Этот вопрос не имеет прямого отношения к тестированию и TDD.
`
POST /user/isValidEmail
POST /user/isValidUsername
`
И да. Наличие такого метода это серьёзная дыра в безопастности позволяющая собрать базу email ваших пользователей. На месте бизнеса я бы ещё подумал что хуже — уменьшение конверсии или утечка персональных данных пользователей.
Да. Проверить наличие совпадений до отправки может быть удобно, но без этого можно обойтись
Первое что пришло в голову. На первом шаге мы отправляем запрос на адрес:
POST /insurance/{code}/user
code — соответственно номер страховки
В случае ошибки переходим к шагу 2 и отправляем запрос:
POST /user/
Принципы REST не нарушены. Интерфейс вполне логичен
миграции применяются при деплое автоматически или вручную. Сначала на тестовом сервере с использованием CI, а потом на боевом.
запихнул миграцию в транзакцию и если она не выполнилась, транзакцию откатываем. Это делается через систему мигрирования.
Править миграцию в автоматическом режиме невозможно и не нужно.
Проект имеет доступ к всей базе и менять может всю базу. На уровне миграции разделения прав доступа быть не может. Это задача другого порядка.
миграции нужно хранить в репозитории с кодом
Многие системы миграций умеют генерить код на основе БД и миграции на основе кода. Doctrine Migrations по крайней мере точно умеет
добавляя к коммиту с миграцией номер задачи мы автоматически связываем их. Так работает GitHub и GitLab
Для того что бы увидеть конечный результат нужно применить миграции.
Обычно деплой мастер применяет все миграции на локальной базе и проверяет их на конфликты и потом при желании собирает их в одну большую миграцию.
Я лично считаю не правильным использовать PHP конструкции для описания миграции. Во всех проектах которые я видел миграции описывались как SQL. Где-то это был *.sql файл, где-то PHP класс в котором описывались изменения как SQL. Для Yii можно вызвать execute(), для Doctrine Migrations addSql()
Если миграции пишет разработчик БД, а не разработчик приложения, он все равно должен сохранить миграцию в проекте. Для упрощения можно описывать миграции, как говорил AlexLeonov, в отдельный *.sql файлах. Любой нормальный редактор будет поддерживать подсветку SQL синтаксиса в таких файлах.
Я в PHP использую DoctrineMigrations
Я, как и другие пользователи хабра, не знаем ничего. Абсолютно ничего о вашем проекте и о проблемах с которыми вы сталкиваетесь.
Подведем итоги:
1. Вам нужно было в статья привести пример проблемы который вы озвучили в комментариях, тогда бы ваша статья была бы понятней и не вызвала бы такую бурю негативных эмоций. Ваше решение, в отрыве от решаемой им проблемы, выглядит бессмысленно и бесполезно.
2. Кроме вашего DataObject есть и другие методы решения описанной проблемы, но в целом оно вполне имеет право на жизнь.
Спасибо за интересную дискуссию. На этой замечательной ноте предлагаю закруглится.
Получается так. Есть базовый, не изменяемый функционал. И создаются новые плагины которые расширяют базовый функционал не затрагивая базовый класс. Базовый класс изменять условно нельзя.
В таком случае нужно сразу сказать что дополнительные поля из плагинов не должны быть обязательными для заполнения, иначе они могут поломать базовый функционал.
Я вижу 2 пути решения проблемы. Вариант с хранением в БД JSON я не рассматриваю потому что это… извращение. В таком случает лучше сразу хранить все данные в документоориентированных СУБД.
1. Создание отдельной таблицы с дополнительными полями необходимыми для плагина и сделать связь с базовой таблицей OneToOne.
Преимущества:
Ни базовый класс, ни базовая таблица никак не затрагиваются. Новая сущность существует параллельно с базовой. Доступ осуществляется так:
Недостатки:
Нет возможности получить сущность плагина из базовой сущности. Так как мы не можем изменять базовый класс, следующий вариант работать не будет:
Нужно создавать еще одну таблицу и делать JOIN для выбора дополнительных данных
2. Расширить функционал базового класса через extends и новые поля из класса CustomerRef должны просто игнорироваться движком.
Преимущества:
Просто в реализации. Код будет выглядеть так:
Недостатки:
Опять же нельзя получить поля плагина из базового класса.
Усложняется разработка движка который будет отвечать за загрузку/сохранение данных. При неправильной реализации могут потеряться значения дополнительных полей CustomerRef при сохранении объекта как Customer.
Итог
Оба варианты рабочие, но не применимы для тех случаев когда нужно получить дополнительные поля из оригинального объекта. Это не страшно в тех случаях если плагины используются только для создания новой функциональности и фатально для тех случаев когда нужно переопределить базовую функциональность.
Ваше решение с DataObject позволяет получить дополнительные поля из базового класса, хотя автодополнение в IDE работать не будет.
PS: надо отметь что отсутствие возможности получить поля плагина из базового класса не всегда является минусом. В некоторых случаях это позволяет избежать ошибок.
Я не могу вам предложить конкретное решение вашей проблемы потому что не знаю особенностей архитектуры вашего приложение и не знаю на что вы можете влиять. Судя по тому что вы предлагаете в статье, вы можете влиять на код всех 3-х компонентов: «базовый функционал», «плагина 1» и «плагина 2».
Самое простое решение, если вы не можете влиять на код плагинов, это добавить в базовый функционал атрибут $ref из плагина 1 и атрибут $email из плагина 2. Базовый класс будет описывать реальную сущность из БД, а сущности из плагинов будут ориентироваться на свой интерфейсом как и раньше, а дополнительные атрибуты из другого плагина будут просто игнорировать.
Пример решения с использованием декораторов и включенным strict mode:
под рукой нет php7 чтобы проверить, но должно работать
исполняемый код полностью копирует ваш:
С использованием DataObject нельзя контролировать тип хранящихся в нем данных и список самих данных. Например что будет при выполнении следующего кода? Вы будете в методе save() проверять список доступных полей на запись или будете писать данные как есть в БД и получать fatal error из-за отсутствия соответствующей колонки в таблице?
Для этого создается обычный класс. DataObject здесь не нужен. Он не решает никаких проблем, а только создает их
Множественное наследование и не должно работать. В вашем случае нужно использовать Адаптор. Использование DataObject в описанном вами случае больше похоже на костыль
Поэтому нужно использовать классы которые будут описывать конкретную структуру «известных данных», а не ваш DataObject и массив.
Пример по вашем же данным
И при правильном описании аннотации будет работать автодополнение на протчжении всей цепочки вызовов
Если же нужен контейнер для временного хранения данных, то, как и говорили michael_vostrikov и lair, достаточно обычного массива
По поводу вашего DataObject. Я подобное создавал в первый год изучения прогркммирования и очень бытсро отказался от этого решения. Основная проблема это отсутствие типизации и жесткой структуры объектов и с автодополнением в те годы были проблемы.
Voter может быть полезен при использования SonataAdminBundle.
В конфигах включаешь проверку
и в Voter::getSupportedAttributes() можно возвращать стандартные роли типа:
это позволит управлять доступом в админке без явного вызова функции denyAccessUnlessGranted и не меняя код админки вообще
Получается мы повышаем безопастность сайта в ущерб производительности. По моему это не совсем правильно. Поправьте меня если я ошибаюсь.
В таком случае интеграционные тесты имеют чуть больший процент покрытия чем модульные, но все так же далеки от 100%.
Хорошо. Тогда объясните что вы имеете в виду под словом функционал (feature)? Тестирование только особенностей тестируемого метода, без привязки к зависимостям? Если да, то тестирование получается не полным как и в случае с модульным. И мы опять возвращаемся к проблеме с Петей и John, ибо неполный тест может не отлавливать ситуации при которых Петя == John.
я говорил вот об этом
Если повсеместно использовать DI то этой проблемы не будет и не будет разницы какие тесты вы пишете, интеграционные или модульные. Это организация процесса разработки и архитектуры приложения, ну и обучения джунов. Этот вопрос не имеет прямого отношения к тестированию и TDD.