или
Обычно мы решаем проблемы заказчика
но иногда вместе с этим добавляем новых
когда не используем инструменты по назначению.
Семантика – значение, смысл
(отдельного слова, оборота речи, ...)
<body>
<div>
<div>
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/contact">Contact</a>
</div>
</div>
<div>
<div>
<h1>First article</h1>
<div>2014.12.06</div>
</div>
<p>
Lorem Ipsum is simply dummy text of
the printing and typesetting industry.
</p>
<div>
Some, Super, Tags
</div>
</div>
<div>© Site.com</div>
</body>
<body>
<header>
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
</header>
<article>
<header>
<h1>First article</h1>
<div>2014.12.06</div>
</header>
<p>
Lorem Ipsum is simply dummy text of
the printing and typesetting industry.
</p>
<footer>
Some, Super, Tags
</footer>
</article>
<footer>© Site.com</footer>
</body>
Семантика – использование вещей по смыслу
class MarketController extends Controller
{
public function behaviors() {
return [
'access' => [
'class' => AccessControl::className(),
'rules' => [
[
'allow' => true,
'roles' => ['@'],
],
],
],
];
}
public function actionIndex()
{
return $this->render('index');
}
}
class MarketController extends Controller
{
...
public function actionIndex()
{
$user = Yii::$app->user->identity;
if (empty($user->email) || !$user->emailConfirmed) {
Yii::$app->session->setFlash('error', 'Подтвердите email для торгов.');
return $this->redirect([
'/profile/view',
'return' => Yii::$app->request->url
]);
}
return $this->render('index');
}
}
Что если много action*?
class MarketController extends Controller
{
public function beforeAction($action)
{
if (!parent::beforeAction($action)) {
return false;
}
$user = Yii::$app->user->identity;
if (empty($user->email) || !$user->emailConfirmed) {
Yii::$app->session->setFlash('error', 'Подтвердите email для торгов.');
$this->redirect([
'/profile/view',
'return' => Yii::$app->request->url
]);
return false;
}
return true;
}
...
}
Что если много контроллеров?
namespace app\filters;
use Yii;
use yii\base\ActionFilter;
use yii\helpers\Url;
class MarketAccess extends ActionFilter
{
public function beforeAction($action)
{
$user = Yii::$app->user->identity;
if (empty($user->email) || !$user->emailConfirmed) {
Yii::$app->session->setFlash('error', 'Подтвердите email для торгов.');
Yii::$app->response->redirect(Url::to([
'/profile/view',
'return' => Yii::$app->request->url
]));
return false;
}
return true;
}
}
class MarketController extends Controller
{
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::className(),
'rules' => [
[
'allow' => true,
'roles' => ['@'],
],
],
],
MarketAccess::className(),
];
}
public function actionIndex()
{
return $this->render('index');
}
}
Отделение обязанности проверки
Чистый контроллер без if-ов
Повторное использование в других контроллерах
Тестирование поведения
Отделение бизнес-логики
class MarketController extends \yii\web\Controller
{
public function behaviors()
{
return [
MarketAccess::className(), // setFlash + redirect
];
}
}
class MarketController extends \yii\rest\Controller
{
public function behaviors()
{
return [
MarketAccess::className(), // setFlash (?) + redirect (?)
];
}
}
setFlash (?) + redirect (?)
Может разделить?
class MarketController extends \yii\web\Controller
{
public function behaviors()
{
return [
WebMarketAccess::className(), // setFlash + redirect
];
}
}
class MarketController extends \yii\rest\Controller
{
public function behaviors()
{
return [
ApiMarketAccess::className(), // BadRequestHttpException
];
}
}
И внутри...
class WebMarketAccess extends ActionFilter
{
public function beforeAction($action)
{
$user = Yii::$app->user->identity;
if (empty($user->email) || !$user->emailConfirmed) {
Yii::$app->session->setFlash('error', 'Подтвердите email для торгов.');
Yii::$app->response->redirect(Url::to([
'/profile/view',
'return' => Yii::$app->request->url
]));
return false;
}
return true;
}
}
setFlash + redirect
class ApiMarketAccess extends ActionFilter
{
public function beforeAction($action)
{
$user = Yii::$app->user->identity;
if (empty($user->email) || !$user->emailConfirmed) {
throw new BadRequestHttpException('Подтвердите email для торгов.');
}
return true;
}
}
BadRequestHttpException
class WebMarketAccess extends ActionFilter
{
public function beforeAction($action)
{
if (empty($user->email) || !$user->emailConfirmed) {
...
}
}
}
class ApiMarketAccess extends ActionFilter
{
public function beforeAction($action)
{
if (empty($user->email) || !$user->emailConfirmed) {
...
}
}
}
повторяется
class WebMarketAccess extends ActionFilter
{
public function beforeAction($action)
{
if (!$user->isEmailConfirmed()) {
...
}
}
}
class ApiMarketAccess extends ActionFilter
{
public function beforeAction($action)
{
if (!$user->isEmailConfirmed()) {
...
}
}
}
class User extends ActiveRecord implements IdentityInterface
{
...
public function isEmailConfirmed()
{
return !empty($this->email) && $this->emailConfirmed;
}
}
class WebMarketAccess extends ActionFilter
{
public function beforeAction($action)
{
if (!$user->isEmailConfirmed()) {
...
}
}
}
class ApiMarketAccess extends ActionFilter
{
public function beforeAction($action)
{
if (!$user->isEmailConfirmed()) {
...
}
}
}
проверки спрятаны...
class WebMarketAccess extends ActionFilter
{
public function beforeAction($action)
{
if (!$user->isEmailConfirmed() || $this->billing->hasDebts($user->id)) {
...
}
}
}
class ApiMarketAccess extends ActionFilter
{
public function beforeAction($action)
{
if (!$user->isEmailConfirmed() || $this->billing->hasDebts($user->id)) {
...
}
}
}
усложнится логика...
namespace app\services;
use app\models\User;
class MarketAccessChecker
{
private $billing;
public function __construct(Billing $billing)
{
$this->billing = $billing;
}
public function allowToMarket(User $user)
{
return $user->isEmailConfirmed() && !$this->billing->hasDebts($user->id);
}
}
инвертируя условие
class ApiMarketAccess extends ActionFilter
{
private $checker;
public function __construct(MarketAccessChecker $checker, array $config = [])
{
$this->checker = $checker;
parent::__construct($config);
}
public function beforeAction($action)
{
$user = Yii::$app->user->identity;
if (!$this->checker->allowToMarket($user)) {
throw new BadRequestHttpException('Подтвердите email и погасите долги.');
}
return true;
}
}
class WebMarketAccess extends ActionFilter
{
public function beforeAction($action)
{
if (!$this->checker->allowToMarket(Yii::$app->user->identity)) {
Yii::$app->session->setFlash('error', 'Подтвердите email и ...');
Yii::$app->response->redirect(...);
return false;
}
return true;
}
}
class ApiMarketAccess extends ActionFilter
{
public function beforeAction($action)
{
if (!$this->checker->allowToMarket(Yii::$app->user->identity)) {
throw new BadRequestHttpException('Подтвердите email и ...');
}
return true;
}
}
Отделение бизнес-логики
Unit-тестирование
class CatalogController extends Controller
{
public function actionCategory($slug)
{
$category = $this->findCategory($slug);
$query = Product::find()
->andWhere(['status' => Product::STATUS_PUBLISHED])
->andWhere(['<=', 'published_from', time()])
->andWhere(['category_id' => $category->id]);
$dataProvider = new ActiveDataProvider([
'query' => $query,
]);
return $this->render('category', [
'dataProvider' => $dataProvider,
]);
}
...
}
class CatalogController extends Controller
{
public function actionProduct($id)
{
return $this->render('product', [
'product' => $this->findProduct($id),
]);
}
private function findProduct($id)
{
$product = Product::find()
->andWhere(['status' => Product::STATUS_PUBLISHED])
->andWhere(['<=', 'published_from', time()])
->andWhere(['id' => $id])
->one();
if (!$product) {
throw new NotFoundHttpException();
}
return $product;
}
}
class CatalogController extends Controller
{
public function actionCategory($slug)
{
$query = Product::find()
->andWhere(['status' => Product::STATUS_PUBLISHED])
->andWhere(['<=', 'published_from', time()])
->andWhere(['category_id' => $category->id]);
...
}
private function findProduct($id)
{
$product = Product::find()
->andWhere(['status' => Product::STATUS_PUBLISHED])
->andWhere(['<=', 'published_from', time()])
->andWhere(['id' => $id])
->one();
...
}
}
namespace app\models\query;
use app\models\Product;
use yii\db\ActiveQuery;
class ProductQuery extends ActiveQuery
{
public function published()
{
return $this
->andWhere(['status' => Product::STATUS_PUBLISHED])
->andWhere(['<=', 'published_from', time()]);
}
}
namespace app\models;
use app\models\query\ProductQuery;
use yii\db\ActiveRecord;
class Product extends ActiveRecord
{
const STATUS_DRAFT = 0;
const STATUS_PUBLISHED = 1;
public static function find()
{
return new ProductQuery(get_called_class());
}
}
class CatalogController extends Controller
{
public function actionCategory($slug)
{
$query = Product::find()
->andWhere(['status' => Product::STATUS_PUBLISHED])
->andWhere(['<=', 'published_from', time()])
->andWhere(['category_id' => $category->id]);
...
}
private function findProduct($id)
{
$product = Product::find()
->andWhere(['status' => Product::STATUS_PUBLISHED])
->andWhere(['<=', 'published_from', time()])
->andWhere(['id' => $id])
->one();
...
}
}
class CatalogController extends Controller
{
public function actionCategory($slug)
{
$query = Product::find()->published()
->andWhere(['category_id' => $category->id]);
...
}
private function findProduct($id)
{
$product = Product::find()->published()
->andWhere(['id' => $id])
->one();
...
}
}
Остаётся category_id
class ProductQuery extends ActiveQuery
{
public function published()
{
return $this
->andWhere(['status' => Product::STATUS_PUBLISHED])
->andWhere(['>=', 'published_from', time()]);
}
public function forCategory($id)
{
return $this->andWhere(['category_id' => $id]);
}
}
$query = Product::find()
->andWhere(['status' => Product::STATUS_PUBLISHED])
->andWhere(['<=', 'published_from', time()])
->andWhere(['category_id' => $category->id]);
$dataProvider = new ActiveDataProvider([
'query' => $query,
]);
vs
$dataProvider = new ActiveDataProvider([
'query' => Product::find()->published()->forCategory($category->id),
]);
Понятнее
Проще
Надёжнее
Message::find()->unread()->forUser($id)->byLastWeek();
Request::find()->published()->fromUser($id)->withoutResponses();
Dialog::find()->betweenUsers($fromId, $toId)->withNewMessages();
Man::find()->holost()->sKvartiroy()->bezVP()->sDohodomOt(2000, 'USD')->...
Смысловой язык
class ProductQuery extends ActiveQuery
{
public function forCategory($id)
{
return $this->andWhere(['category_id' => $id]);
}
}
может быть сложнее...
class ProductQuery extends ActiveQuery
{
public function forCategory($id)
{
$ids = [$id];
$childrenIds = [$id];
while (
$childrenIds = Category::find()
->select('id')
->andWhere(['parent_id' => $childrenIds])
->column()
) {
$ids = array_merge($ids, $childrenIds);
}
return $this->andWhere(['category_id' => $ids]);
}
}
чтобы учесть вложенность
class CategoriesWidget extends Widget
{
public function run()
{
$aggData = (new \yii\elasticsearch\Query())->from('shop', 'products')
->addAggregation('product_counts', 'terms', ['field'=>'category_id'])
->search(null, ['search_type' => 'count']);
$buckets = $aggData['aggregations']['product_counts']['buckets'];
$counts = ArrayHelper::map($buckets, 'key', 'doc_count');
$categories = Category::find()->orderBy('name')->all();
return Menu::widget([
'items' => array_map(function (Category $category) use ($counts) {
$count = ArrayHelper::getValue($counts, $category->id, 0);
return [
'label' => $category->name . ' (' . $count . ')',
'url' => ['/catalog/category', 'id' => $category->id],
];
}, $categories),
]);
}
}
namespace app\repositories;
class CategoryRepository
{
public function getAllWithCounts()
{
$aggData = (new \yii\elasticsearch\Query())->from('product', 'products')
->addAggregation('product_counts', 'terms', ['field'=>'category_id'])
->search(null, ['search_type' => 'count']);
$buckets = $aggData['aggregations']['product_counts']['buckets'];
$counts = ArrayHelper::map($buckets, 'key', 'doc_count');
$categories = Category::find()->orderBy('name')->all();
return array_map(function (Category $category) use ($counts) {
$count = ArrayHelper::getValue($counts, $category->id, 0);
return [
'category' => $category,
'count' => $count,
];
}, $categories);
}
}
namespace app\viewModel;
use app\models\Category;
class CategoryWithCount
{
public $category;
public $count;
public function __construct(Category $category, $count)
{
$this->category = $category;
$this->count = $count;
}
}
namespace app\repositories;
class CategoryRepository
{
/**
* @return CategoryWithCount[]
*/
public function getAllWithCounts()
{
$aggData = (new \yii\elasticsearch\Query())->from('product', 'products')
->addAggregation('product_counts', 'terms', ['field'=>'category_id'])
->search(null, ['search_type' => 'count']);
$buckets = $aggData['aggregations']['product_counts']['buckets'];
$counts = ArrayHelper::map($buckets, 'key', 'doc_count');
$categories = Category::find()->orderBy('name')->all();
return array_map(function (Category $category) use ($counts) {
$count = ArrayHelper::getValue($counts, $category->id, 0);
return new CategoryWithCount($category, $count);
}, $categories);
}
}
class CategoriesWidget extends Widget
{
public function run()
{
$aggData = (new \yii\elasticsearch\Query())->from('product', 'products')
->addAggregation('product_counts', 'terms', ['field'=>'category_id'])
->search(null, ['search_type' => 'count']);
$buckets = $aggData['aggregations']['product_counts']['buckets'];
$counts = ArrayHelper::map($buckets, 'key', 'doc_count');
$categories = Category::find()->orderBy('name')->all();
return Menu::widget([
'items' => array_map(function (Category $category) use ($counts) {
$count = ArrayHelper::getValue($counts, $category->id, 0);
return [
'label' => $category->name . ' (' . $count . ')',
'url' => ['/catalog/category', 'id' => $category->id],
];
}, $categories),
]);
}
}
class CategoriesWidget extends Widget
{
private $categories;
public function __construct(CategoryRepository $categories, $config = [])
{
$this->categories = $categories;
parent::__construct($config);
}
public function run()
{
return Menu::widget([
'items' => array_map(function (CategoryWithCount $item) {
return [
'label' => $item->category->name . ' (' . $item->count . ')',
'url' => ['/catalog/category', 'id' => $item->category->id],
];
}, $this->categories->getAllWithCounts()),
]);
}
}
class PasswordResetRequestForm extends Model
{
public function sendEmail()
{
$user = User::findOne([
'status' => User::STATUS_ACTIVE,
'email' => $this->email,
]);
if (!$user) {
return false;
}
if (!User::isPasswordResetTokenValid($user->password_reset_token)) {
$user->generatePasswordResetToken();
if (!$user->save()) {
return false;
}
}
return Yii::$app->mailer
->compose('passwordResetToken', ['user' => $user])
->setTo($this->email)
->send();
}
}
class SiteController extends Controller
{
public function actionRequestPasswordReset()
{
$model = new PasswordResetRequestForm();
if ($model->load(Yii::$app->request->post()) && $model->validate()) {
if ($model->sendEmail()) {
Yii::$app->session->setFlash('success', 'Check your email.');
return $this->goHome();
} else {
Yii::$app->session->setFlash('error', 'Sorry, we are unable...');
}
}
return $this->render('requestPasswordResetToken', [
'model' => $model,
]);
}
}
Sorry, не смогла... Логи пустые...
Напишу-ка в обратную связь... Тоже не смогла...
public function sendEmail()
{
if (!$user = User::findOne([
'status' => User::STATUS_ACTIVE,
'email' => $this->email,
])) {
throw new \DomainException('User is not found.');
}
if (!User::isPasswordResetTokenValid($user->password_reset_token)) {
$user->generatePasswordResetToken();
if (!$user->save()) {
throw new \RuntimeException('User saving error.');
}
}
if (!Yii::$app->mailer
->compose('passwordResetToken', ['user' => $user])
->setTo($this->email)
->send()) {
throw new \RuntimeException('Email sending error.');
}
}
class SiteController extends Controller
{
public function actionRequestPasswordReset()
{
$model = new PasswordResetRequestForm();
if ($model->load(Yii::$app->request->post()) && $model->validate()) {
if ($model->sendEmail()) {
Yii::$app->session->setFlash('success', 'Check your email.');
return $this->goHome();
} else {
Yii::$app->session->setFlash('error', 'Sorry, we are unable...');
}
}
return $this->render('requestPasswordResetToken', [
'model' => $model,
]);
}
}
class SiteController extends Controller
{
public function actionRequestPasswordReset()
{
$model = new PasswordResetRequestForm();
if ($model->load(Yii::$app->request->post()) && $model->validate()) {
try {
$model->sendEmail();
Yii::$app->session->setFlash('success', 'Check your email.');
return $this->goHome();
} catch (\Exception $e) {
Yii::$app->errorHandler->logException($e);
Yii::$app->session->setFlash('error', 'Sorry, we are unable...');
}
}
return $this->render('requestPasswordResetToken', [
'model' => $model,
]);
}
}
<?php $form = ActiveForm::begin(); ?>
<?= $form->field($model, 'categoryId')->dropDownList(
ArrayHelper::map(Category::find()->asArray()->all(), 'id', 'name')
) ?>
<?= $form->field($model, 'name')->textInput() ?>
<?= $form->field($model, 'text')->textarea(['rows' => 20]) ?>
...
<?php ActiveForm::end(); ?>
Много кода в представлениях
Особенно для дерева
class PostForm extends Model
{
public $title;
public $categoryId;
public $text;
public function rules()
{
return [
[['title', 'categoryId'], 'required'],
[['title', 'text'], 'string'],
[['categoryId'], 'integer'],
];
}
public function categoriesList()
{
return ArrayHelper::map(Category::find()->asArray()->all(), 'id', 'name');
}
}
<?php $form = ActiveForm::begin(); ?>
<?= $form->field($model, 'categoryId')->dropDownList(
ArrayHelper::map(Category::find()->asArray()->all(), 'id', 'name')
) ?>
<?= $form->field($model, 'name')->textInput() ?>
<?= $form->field($model, 'text')->textarea(['rows' => 20]) ?>
...
<?php ActiveForm::end(); ?>
<?php $form = ActiveForm::begin(); ?>
<?= $form->field($model, 'categoryId')->dropDownList($model->categoriesList()) ?>
<?= $form->field($model, 'name')->textInput() ?>
<?= $form->field($model, 'text')->textarea(['rows' => 20]) ?>
...
<?php ActiveForm::end(); ?>
class SignupForm extends Model
{
public $username;
public $email;
public $phone;
public $password;
public function rules()
{
return [
[['username', 'email', 'phone', 'password'], 'required'],
['username', 'match', 'pattern' => '#^[a-z0-9-]+$#is'],
['phone', 'match', 'pattern' => '#^7\d+$#s'],
['email', 'email'],
];
}
}
class ProfileForm extends Model
{
public $email;
public $phone;
public function rules()
{
return [
[['email', 'phone'], 'required'],
['phone', 'match', 'pattern' => '#^7\d+$#s'],
['email', 'email'],
];
}
}
Регулярки повторяются
namespace app\validators;
use yii\validators\RegularExpressionValidator;
class UsernameValidator extends RegularExpressionValidator
{
public $pattern = '#^[a-z0-9_-]+$#is';
}
...
class PhoneValidator extends RegularExpressionValidator
{
public $pattern = '#^7\d+$#s';
}
class SignupForm extends Model
{
public $username;
public $email;
public $phone;
public $password;
public function rules()
{
return [
[['username', 'email', 'phone', 'password'], 'required'],
['username', UsernameValidator::className()],
['phone', PhoneValidator::className()],
['email', 'email'],
];
}
}
/**
* @property integer $status
* @property integer $deliveryDate
*/
class Order extends ActiveRecord
{
const STATUS_NEW = 1;
const STATUS_PAID = 2;
const STATUS_SENT = 3;
const STATUS_CANCELLED = 4;
}
<?php if (
$order->status === Order::STATUS_NEW ||
($order->status === Order::STATUS_PAID &&
$order->deliveryDate > time() + 3600 * 24)
): ?>
<div>...</div>
<?php endif; ?>
1. Information expert
2. Ещё какие-то пункты
class Order extends ActiveRecord
{
const STATUS_NEW = 1;
const STATUS_PAID = 2;
const STATUS_SENT = 3;
const STATUS_CANCELLED = 4;
public function isNew()
{
return $this->status === self::STATUS_NEW;
}
}
<?php if (
$order->status === Order::STATUS_NEW ||
($order->status === Order::STATUS_PAID &&
$order->deliveryDate > time() + 3600 * 24)
): ?>
<div>...</div>
<?php endif; ?>
<?php if (
$order->isNew() ||
($order->status === Order::STATUS_PAID &&
$order->deliveryDate > time() + 3600 * 24)
): ?>
<div>...</div>
<?php endif; ?>
class Order extends ActiveRecord
{
public function isNew()
{
return $this->status === self::STATUS_NEW;
}
public function isPaid()
{
return $this->status === self::STATUS_PAID;
}
}
<?php if (
$order->isNew() ||
($order->status === Order::STATUS_PAID &&
$order->deliveryDate > time() + 3600 * 24)
): ?>
<div>...</div>
<?php endif; ?>
<?php if (
$order->isNew() ||
($order->isPaid() &&
$order->deliveryDate > time() + 3600 * 24)
): ?>
<div>...</div>
<?php endif; ?>
<?php if (
$order->isNew() ||
($order->isPaid() && $order->deliveryDate > time() + 3600 * 24)
): ?>
<div>...</div>
<?php endif; ?>
class Order extends ActiveRecord
{
public function isNew() {
return $this->status === self::STATUS_NEW;
}
public function isPaid() {
return $this->status === self::STATUS_PAID;
}
public function isOnDelivery() {
return $this->deliveryDate < time() + 3600 * 24;
}
}
<?php if (
$order->isNew() ||
($order->isPaid() && $order->deliveryDate > time() + 3600 * 24)
): ?>
<div>...</div>
<?php endif; ?>
<?php if (
$order->isNew() ||
($order->isPaid() && !$order->isOnDelivery())
): ?>
<div>...</div>
<?php endif; ?>
<?php if ($order->isNew() || ($order->isPaid() && !$order->isOnDelivery())): ?>
<div>...</div>
<?php endif; ?>
class Order
{
public function isNew() {
return $this->status == self::STATUS_NEW;
}
public function isPaid() {
return $this->status == self::STATUS_PAID;
}
public function isOnDelivery() {
return $this->deliveryDate < time() + 3600 * 24;
}
public function isCancelable() {
return $this->isNew() || ($this->isPaid() && !$this->isOnDelivery());
}
}
<?php if ($order->isNew() || ($order->isPaid() && !$order->isOnDelivery())): ?>
<div>...</div>
<?php endif; ?>
<?php if ($order->isCancelable()): ?>
<div>...</div>
<?php endif; ?>
<?php if (
$order->status === Order::STATUS_NEW ||
($order->status === Order::STATUS_PAID &&
$order->deliveryDate > time() + 3600 * 24)
): ?>
<?= Html::a('Отменить', ['cancel', 'id' => $order->id]) ?>
<?php endif; ?>
vs
<?php if ($order->isCancelable()): ?>
<?= Html::a('Отменить', ['cancel', 'id' => $order->id]) ?>
<?php endif; ?>
Понятнее
Проще
Надёжнее
$order->getAddress()
$order->isPaid()
$order->hasProduct($productId)
$order->canAddNewItem()
$order->canBePaidByUser($userId)
читают/считают
/**
* @property integer $status
* @property integer $deliveryDate
*
* @property Member[] $members
*/
class Order extends ActiveRecord
{
public function getMembers()
{
return $this->hasMany(Member::className(), ['order_id', 'id']);
}
}
Много участников
/**
* @property integer $order_id
* @property integer $user_id
* @property integer $amount
* @property integer $status
*/
class Member extends ActiveRecord
{
const STATUS_WAITING = 1;
const STATUS_PAID = 2;
public function getUser()
{
return $this->hasOne(User::className(), ['id', 'user_id']);
}
}
class Order extends ActiveRecord
{
...
public function canBePaidByUser($userId)
{
// Если заказ не в стадии оплаты, то не может:
if (!$this->isWaitingForPayment()) {
return false;
}
// Если является участником, но ещё не платил, то может:
foreach ($this->members as $member) {
if ($member->isForUser($userId) && $member->isWaiting()) {
return true;
}
}
// Иначе не может:
return false;
}
}
class Member extends ActiveRecord
{
const STATUS_WAITING = 1;
const STATUS_PAID = 2;
public function isForUser($id)
{
return $this->user_id === $id;
}
public function isWaiting()
{
return $this->status === self::STATUS_WAITING;
}
}
<?php if ($order->canBePaidByUser(Yii::$app->user->id)): ?>
<?= Html::a('Оплатить', ['pay', 'id' => $order->id]) ?>
<?php endif; ?>
class Order extends ActiveRecord
{
...
public function payByUser($userId, $amount)
{
if (!$this->isWaitingForPayment()) {
throw new \DomainException('Оплата заказа недоступна.');
}
foreach ($this->members as $member) {
if ($member->isForUser($userId)) {
$member->pay($amount);
return;
}
}
throw new \DomainException('Заявка не найдена.');
}
}
class Member extends ActiveRecord
{
...
public function pay($amount)
{
if (!$this->isWaiting()) {
throw new \DomainException('Заявка уже оплачена.');
}
$this->amount = $amount;
$this->status = self::STATUS_PAID;
}
}
namespace app\services;
use app\models\User;
class MarketAccessChecker
{
private $billing;
public function __construct(Billing $billing)
{
$this->billing = $billing;
}
public function allowToMarket(User $user)
{
return $user->isEmailConfirmed() && !$this->billing->hasDebts($user->id);
}
}
- http://elisdn.ru
- http://elisdn.ru/oop-week
- http://elisdn.ru/yii2-shop
- http://slides.elisdn.ru/2017/yiiconf-semantic