Документ описывает соглашения, преследующие цель сделать проект более последовательным и предсказуемым.
В основе положены стандарты PSR-1 и PSR-2, однако есть небольшие поправки и дополнения, а именно:
<?php и никакие другие (короткие) теги не приемлемыВ нашей команде мы придерживаемся спецификации OpenApi 3.0.2. Быстро войти в курс дел помогут примеры с успешными запросами (можно поиграться).
О том, в какой форме возвращать ответы написано тут
Часто возникает вопрос о том, как описывается результат с ошибкой, поэтому наглядный пример:
Error:
type: object
properties:
code:
type: string
message:
type: string
required:
- code
- message
Соответственно для поля code нужно указывать одно из значений \Gs\Core\Lib\Helper\Http\ResponseStatus
p.s. если будем описывать свои запросы/ответы с помощью https://github.com/zircote/swagger-php то бонусом получим автоматическую документацию.
Мы придерживаемся SOA, а это накладывает требование: работать с объектами, как с сервисами (читайте любой объект класса === сервис). Отличие сервиса от обычного объекта в том, что сервис приобретает состояние во время создания (инициализации через метод __construct) и более не меняет свое состояние (если не брать во внимание изменение данных в зависимостях сервивиса, то можно считать его идемпотентным ). Сервисы лежат в сервис-контейнере (далее контейнер), а чтобы сервис попал в контейнер, сервис нужно зарегистрировать в контейнере. Регистрация в контейнере делается с помощью класса::метода DependencyInjectionProvider::initServices После того, как сервис зарегистрирован, с ним можно работать используется вызов \Container::get($serviceName). Приведем пример:
У нас есть класс \Gs\Core\Lib\Helper\Encoding\EncodingHelper этот класс в рамках SOA является сервисом.
Если мы откроем \Gs\Core\Configuration\DependencyInjectionProvider::initServices то увидим там регистрацию данного сервиса в контейнере:
\Container::make('Gs\Core\Lib\Helper\Encoding\EncodingHelper');
Теперь, если открыть \Gs\Gemopay\Service\Payment\PaymentService::setPaymentOriginatorName то мы увидим, что обратиться к данному сервису (объекту) можно с помощью метода:
\Container::get('Gs\Core\Lib\Helper\Encoding\EncodingHelper')
Важно: сервис это всегда объект, у него не может быть статичных методов.
Работу с данными мы производим двумя способами:
Мы избегаем понятия толстая модель, как и избегаем понятия тонкая модель таким образом наши принципы нашей доменной модели:
Ниже приводится пример PatientModel и PatientModelCondition + даны комментарии.
<?php
namespace Gs\Gemopay\Entity\Payment;
/**
* @property integer $id
* @property integer $order_num
* @property string $date
* @property float $total
* @property OrderModel $order
*/
class PaymentModel extends PaymentModelCondition
{
// обязательный метод, используется для того, чтобы понять, с какой таблицей бд будет выполняться взаимодействия
public function tableName()
{
return TableNameEnum::PAYMENT;
}
// обязательный метод, используется для простого поиска модели (использование статичного вызова разрешено в качестве исключения)
public static function model($className = __CLASS__)
{
return parent::model($className);
}
// необязательный метод для задания правил валидации полей при определенных действиях (update, insert)
public function rules()
{
return [
[
PaymentFieldEnum::DATE_UPDATED,// это поле может быть с именем 'updated_at' или похожим
'default',
'value' => new \CDbExpression('GETDATE()'),
'setOnEmpty' => false,
'on' => 'update'
],
[
PaymentFieldEnum::DATE_INSERTED . ',' . PaymentFieldEnum::DATE_UPDATED,
'default',
'value' => new \CDbExpression('GETDATE()'),
'setOnEmpty' => false,
'on' => 'insert'
]
];
}
/**
* Изменение значения полей предпочтительно осуществлять с помощью set-ров,
* это позволяет исключить неявную работу с данными объекта (инкапсуляция данных),
* в подарок мы получаем fluent interface - https://ru.wikipedia.org/wiki/Fluent_interface#PHP, пример:
*
* @return $this
*/
public function setName(string $value): self// set-метод в PHP-7 позволяет валидировать данные в момент изменения
{
$this->name = $value;
return $this;
}
}
/**
* Этот класс содержит функции добавляющие условия поиска
*
* Документация: https://www.yiiframework.com/doc/guide/1.1/ru/database.ar#sec-12)
*
* Функции в этом классе должны быть назван согласно одному из правил:
* - by[имя поля или бизнес-описание в CamelCase]
* - where[бизнес-описание в CamelCase]
*
* Примеры использования:
* - $payments = PaymentModel::model()->byOrderNumber(123)->find();
* - $payments = PaymentModel::model()->wherePatientIsMain()->find();
**/
class PaymentModelCondition extends \Gs\Core\Lib\Yii\Component\Model\CActiveRecord
{
// может использоваться в цепочке условий:
/**
* Условие: поиск по номеру заказа
*
* @return $this
*/
public function byOrderNumber($orderNumber)
{
$orderNumber = $this->getDbConnection()->quoteValue($orderNumber);
$this->getDbCriteria()->mergeWith([
'condition' => PaymentModelFieldEnum::ORDER_NUMBER . ' = ' . $orderNumber,
]);
return $this;
}
/**
* Условие: пациент должен быть основным (или не иметь подтверждения связи с каким-либо основным пациентом)
*
* @return $this
*/
public function wherePatientIsMain()
{
$this->getDbCriteria()->addCondition('(' .
$this->getTableAlias() . '.' . PatientFieldEnum::PARENT_ID . ' = '. ParentIdValueEnum::NOT_SPECIFIED . ' OR ' .
$this->getTableAlias() . '.' . PatientFieldEnum::PARENT_ID_CONFIRMED_FLAG . ' = '. ParentIdConfirmedFlagEnum::NOT_CONFIRMED .
')');
return $this;
}
// необязательный метод для связи текущей модели с моделью SecondModelName
public function relations()
{
return array(
'some_relation_name' => array(self::BELONGS_TO, 'SecondModelName', ['field_name_of_second_model' => 'field_name_of_this_model']),
);
}
// правила описанные тут, должны касаться только таблицы данной модели (никаких условий выборки др. моделей или джоина др. таблиц мы тут не описываем)
public function scopes()
{
return array(
'published'=>array(
'condition'=>'status=1',
),
'recently'=>array(
'order'=>'create_time DESC',
'limit'=>5,
),
);
}
}
Теперь рассмотрим создание методов в Dsp (Data Source Provider).
<?php
// для начала пример плохого кода (SQL-запросы частично повторяют друг друга):
class UserDsp implements DataSourceProviderInterface
{
//...
/**
* Найти емэйлы авторизованных пользователей с заданным именем
*
* @param string $name
* @return \CDbDataReader
*/
public function getAuthorizedUsers($name)
{
return $this->db->createCommand('SELECT email FROM users WHERE flag_deleted = 0 AND username = :name GROUP BY email')
->bindValues([':name' => $name])->query();
}
/**
* Найти емэйлы пользователей с заданным статусом
*
* @param string $userStatus
* @return \CDbDataReader
*/
public function getUsersByStatus($userStatus)
{
return $this->db->createCommand('SELECT email FROM users WHERE flag_deleted = 0 AND user_status = :user_status GROUP BY email')
->bindValues([':user_status' => $userStatus])->query();
}
}
// Часть SQL-запроса функции getUsersByStatus повторяет часть SQL-запрос функции
// `getAuthorizedUsers`, а значит нужно вынести данный SQL-запрос в отдельный метод
// таким образом, должен получиться приватный метод вида getUsers(array $select, array $where):
class UserDsp implements DataSourceProviderInterface
{
// ...
/**
* Найти ID-ки авторизованных пользователей с заданным именем
*
* @param string $name
* @return \CDbDataReader
*/
public function getAuthorizedUsers($name)
{
return $this->getUser(['email'],['username' => $name]);
}
/**
* Найти данные пользователей с заданным статусом
*
* @param string $userStatus
* @return \CDbDataReader
*/
public function getUsersByStatus($userStatus)
{
return $this->getUser(['email'],['user_status' => $userStatus]);
}
/**
* @param array $select
* @param array $where
* @return \CDbDataReader
*/
private function getUsers(array $select, array $where = [])
{
$sql = "SELECT " . implode(",", $select) . " FROM users WHERE flag_deleted = 0 GROUP BY email";
$bindValues = [];
if (count($where) > 0) {
foreach ($where as $fieldName => $fieldValue) {
$sql .= " AND " . $fieldName . " = :" . $fieldName;
$bindValues[":" . $fieldName] = $fieldValue;
}
}
return $this->db->createCommand($sql)->bindValues($bindValues)->query();
}
}
Таким образом, выше представлена не калька, а пример демонстрирующий подход, при котором SQL-запросы не повторяются в Dsp.