Сучасні
веб-рішення з Yii 2

Ievgen Kuzminov, PHP/Ruby Team Lead
10 років у веб розробці

6 років - в MobiDev
PHP та Ruby ТімЛід
Автор Yii2 dev digest
IT блоггер http://stdout.in
І таке... https://twitter.com/iJackUA
Шанувач OpenSource
Обережно,
власна думка
Сучасна веб розробка це...
- Швидкість / Якість / Ціна
- Легко знайти (
замінити) розробника - Передбачуваність
- Потужність інструментів
- Перспектива підтримки технології
- Структура, швидкий старт
- Управління пакетами / залежностями
- Організація бізнес логіки
- Сховища даних
- “Швидко та красиво відобразити дані”
- Розробка API / SPA
- Тестування
PHP це погано ...
не
@fopen('http://example.com/not-existing-file', 'r');)
int strpos ( string $haystack , mixed $needle [, int $offset= 0 ] )
string stristr ( string $haystack , mixed $needle [, bool $before_needle = false ] )
bool in_array ( mixed $needle , array $haystack [, bool $strict ] )
mixed array_search ( mixed $needle , array $haystack [, bool $strict ] )
bool isset ( mixed $var [, mixed $... ] )
bool is_null ( mixed $var )
Фреймворки ...
- ховають погані штуки
- всього лише інструмент (один з ...)
- частіше за все - розумніші і більш продумані, ніж програмісти
-
підвищують коефіцієнт
Швидкість / Якість / Ціна
PHP 7
- х2 покращена продуктивність
- сatchable fatal errors
- Scalar Type Hints & Return Types
- анонімні класси
- Null Coalesce Operator
$username = $_GET['user'] ?? 'nobody'; - ...
HHVM - ще краще...
- строга типізація
- статичний аналіз
- занулююмі типи
- перевірка типу на вході і виході
- асинхронність
- ...
XHP - we need to go deeper...
https://github.com/facebook/xhp-lib
$html = <div id="hello"><p>Guten Tag, Mobidev!</p></div>;
echo $html;
$html = "<div id='hello'><p>Guten Tag, Mobidev!</p></div>";
echo $html;
$html = new :div(['id'=>hello], new :p([], 'Guten Tag, Mobidev!'));
echo $html;
class :example:with-id extends :x:element {
use XHPHelpers;
attribute :xhp:html-element;
protected function render(): XHPRoot {
return <div id={$this->getID()} />;
}
}
// <div id="herp"></div>
var_dump((<example:with-id id="herp" />)->toString());
Parse error: syntax error, unexpected T_PAAMAYIM_NEKUDOTAYIM
- PHP - мова не одного фреймворку
- Шлях до ускладнення / ентерпрайзу
- Головна фішка - просто, швидко, дешево
- Додаємо складності - вбиваємо головну фішку

Почнемо з продуктивності ...


Пакети / PSR-4

"require": {
"php": ">=5.4.0",
"yiisoft/yii2": "*",
"yiisoft/yii2-debug": "*",
"ijackua/yii2-kudos-widget": "dev-master",
"evert/sitemap-php": "*",
"heybigname/backup-manager": "0.3.*",
"dropbox/dropbox-sdk": "*"
...
},
composer global require "fxp/composer-asset-plugin:~1.0.0"
composer create-project /
--prefer-dist yiisoft/yii2-app-basic basic

/frontend
--/config
--/models
--/...
/backend
/console
--/config
--/models
...
/common
/environment
/tests
/...
Dependency injection
class Foo
{
public function __construct(Bar $bar)
{
}
}
$foo = $container->get('Foo');
// which is equivalent to the following:
$bar = new Bar;
$foo = new Foo($bar);
Dependency injection
\Yii::$container->set(
'yii\widgets\LinkPager',
['maxButtonCount' => 5]
);
....
echo \yii\widgets\LinkPager::widget();
echo \yii\widgets\LinkPager::widget(
['maxButtonCount' => 20]
);
Service Locator
'components' => [
'db' => [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=localhost;dbname=demo',
'username' => 'root',
'password' => '',
],
$locator->set('db', [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=localhost;dbname=demo',
'username' => 'root',
'password' => '',
]);
Модель - не бордель
- ActiveRecord
- бізнес логіка
- зв’язані сутності
- скоупи / сценарії
- якась функція... "нехай полежить тут"
- ... 2000+ рядків коду
- ... крах
Покращуємо модель
- ActiveRecord виділяє ActiveQuery
-
Скоупи не плутаються в моделі
- "Зовнішні" операції ~> окремі класі
- Відокремлена логіка ~> сервіс-об’єкти
- Думаємо про Single responsibility
Сховища даних. Cross DB.
- Єдиний інтерфейс
- Зв’язки без JOIN-ів
class Customer extends \yii\db\ActiveRecord
{
public static function tableName()
{
return 'customer';
}
public function getComments()
{
// a customer has many comments
return $this->hasMany(Comment::className(), ['customer_id' => 'id']);
}
}
MySQL
class Comment extends \yii\mongodb\ActiveRecord
{
public static function collectionName()
{
return 'comment';
}
public function getCustomer()
{
// a comment has one customer
return $this->hasOne(Customer::className(), ['id' => 'customer_id']);
}
}
$customers = Customer::find()->with('comments')->all();
MongoDB
Повнотекстовий пошук
Sphinx. ElasticSearch.
$customer = Customer::get(1);
$customer = Customer::find()
->where(['name' => 'test'])->one();
$result = Article::find()
->query(["match" => ["title" => "yii"]])->all();
$query = Article::find()->query([
"fuzzy_like_this" => [
"fields" => ["title", "description"],
"like_text" => "This query will return articles that are similar to this text :-)",
"max_query_terms" => 12
]
]);
Віджети
echo \yii\grid\GridView::widget(
[
'dataProvider' => $dataProvider
]);
....
$form = ActiveForm::begin(['id' => 'login-form']);
echo $form->field($model, 'username')
....
Права користувачів
class SiteController extends Controller
{
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::className(),
'only' => ['login', 'logout', 'signup'],
'rules' => [
[
'allow' => true,
'actions' => ['login', 'signup'],
'roles' => ['?'],
],
[
'allow' => true,
'actions' => ['logout'],
'roles' => ['@'],
],
],
],
];
}
// ...
RBAC
namespace app\rbac;
use yii\rbac\Rule;
/**
* Checks if authorID matches user passed via params
*/
class AuthorRule extends Rule
{
public $name = 'isAuthor';
public function execute($user, $item, $params)
{
return isset($params['post'])
? $params['post']->createdBy == $user
: false;
}
}
REST API
use yii\rest\ActiveController;
class UserController extends ActiveController
{
public $modelClass = 'app\models\User';
public $serializer = [
'class' => 'yii\rest\Serializer',
'collectionEnvelope' => 'items',
];
}
REST API
HTTP/1.1 200 OK
Date: Sun, 02 Mar 2014 05:31:43 GMT
Server: Apache/2.2.26 (Unix) DAV/2 PHP/5.4.20 mod_ssl/2.2.26 OpenSSL/0.9.8y
X-Powered-By: PHP/5.4.20
X-Pagination-Total-Count: 1000
X-Pagination-Page-Count: 50
X-Pagination-Current-Page: 1
X-Pagination-Per-Page: 20
Link: <http://localhost/users?page=1>; rel=self,
<http://localhost/users?page=2>; rel=next,
<http://localhost/users?page=50>; rel=last
Transfer-Encoding: chunked
Content-Type: application/json; charset=UTF-8
{
"items": [
{
"id": 1,
...
}....
],
"_links": {
"self": {
"href": "http://localhost/users?page=1"
},
"next": {
"href": "http://localhost/users?page=2"
},
"last": {
"href": "http://localhost/users?page=50"
}
},
"_meta": {
"totalCount": 1000,
"pageCount": 50,
"currentPage": 1,
"perPage": 20
}
}
REST API
public function fields()
{
return [
'id',
'email' => 'email_address',
'name' => function ($model) {
return $model->first_name . ' ' . $model->last_name;
},
];
}
Assets bundles
class AppAsset extends AssetBundle
{
public $basePath = '@webroot';
public $baseUrl = '@web';
public $css = [
'css/site.css',
];
public $js = [
];
public $depends = [
'yii\web\YiiAsset',
'yii\bootstrap\BootstrapAsset',
];
}
....
AppAsset::register($this);
Assets tools
- Bower
- Grunt / Gulp
- Webpack
- Browserify
- ...
Тестування
Codeception to the rescue!
$I = new AcceptanceTester($scenario);
$I->amGoingTo('try to login with correct credentials');
$loginPage->login('erau', 'password_0');
$I->expectTo('see that user is logged');
$I->seeLink('Logout (erau)');
$I->dontSeeLink('Login');
Командна работа
Міграції
class m140202_203914_multilingual extends \yii\db\Migration
{
public function up()
{
$this->addColumn('ij_post', 'lang', 'character varying');
$this->renameColumn('ij_note', 'text', 'text_en');
}
public function down()
{
$this->removeColumn('ij_post', 'lang');
$this->renameColumn('ij_note', 'text_en', 'text');
}
}
Командна работа
Оточення / Environments
<?php
return [
'components' => [
'db' => [
'enableSchemaCache' => true,
'schemaCacheDuration' => '3600',
]
]
];
/environments/prod/config/web.env.php
- Немає "DotEnv" з коробки
Чого не вистачає Yii 2
Черга повідомлень
"Active Job"
"Active Mail"
UserMailer::mail(['key1'=>'val1'])
->regConfirm(['user'=>$user])
->deliverLater()
MakeLongRunTask::job(['param1'=>'value1'])->performLater()
MakeLongRunTask::job(['param1'=>'value1'])->perform()
\Yii::$app->queue->push($tube, $payload);
$payload = \Yii::$app->queue->pop($tube);
Що скоро буде...
-
Новий сайт
- сучасний дизайн
- зручна документація
-
Ком’юніті з елементами соц. мережі
- пакети / екстеншни
- Q/A
- Jobs
- ...Ваші пропозиції ? :)
Yii 1 ~> Yii 2 ?
- Composer
- Неймспейси
- Структура папок
- Короткий синтаксис віджетів
- Тестування
Composer
"require": {
"php": ">=5.4.0",
"yiisoft/yii": "dev-master",
...
define('ROOT_DIR', realpath(__DIR__ . '/../'));
require ROOT_DIR . '/vendor/autoload.php';
require_once ROOT_DIR . '/vendor/yiisoft/yii/framework/yii.php';
// run app here
/app/www/index.php
/composer.json
Config
return [
'name' => 'My super app',
'controllerNamespace' => 'admin\controllers',
...
'components' => [
'clientScript' => [
'class' => 'common\components\ClientScript'
],
...
/app/config/main.php
Widgets
Yii::$classMap = [
'CWidget' => 'app/components/CWidget.php'
];
/app/www/index.php
/app/components/CWidget.php
public static function runWidget(array $properties = array(), $captureOutput = false)
{
$className = get_called_class();
if ($captureOutput) {
ob_start();
ob_implicit_flush(false);
$widget = Yii::app()->controller->createWidget($className, $properties);
$widget->run();
return ob_get_clean();
} else {
$widget = Yii::app()->controller->createWidget($className, $properties);
$widget->run();
return $widget;
}
}
\CGridView::runWidget([...]);
Невже він закінчив теревенити ?!

Питання ?
Сучасні веб-рішення з Yii 2
By ijack
Сучасні веб-рішення з Yii 2
Сучасні веб-рішення з Yii 2
- 902