Сучасні

веб-рішення з 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